Version bump

This commit is contained in:
Neil Lalonde 2015-02-12 16:34:53 -05:00
commit 3739684164
241 changed files with 15451 additions and 59215 deletions

View File

@ -107,6 +107,7 @@ gem 'ember-rails'
gem 'ember-source', '1.9.0.beta.4'
gem 'handlebars-source', '2.0.0'
gem 'barber'
gem '6to5'
gem 'message_bus'
gem 'rails_multisite', path: 'vendor/gems/rails_multisite'
@ -116,7 +117,9 @@ gem 'eventmachine'
gem 'fast_xs'
gem 'fast_xor'
gem 'fastimage'
# while we sort out https://github.com/sdsykes/fastimage/pull/46
gem 'fastimage_discourse', require: 'fastimage'
gem 'fog', '1.26.0', require: false
gem 'unf', require: false

View File

@ -6,6 +6,10 @@ PATH
GEM
remote: https://rubygems.org/
specs:
6to5 (0.5.0)
6to5-source (>= 1.14, < 4)
execjs (~> 2.0)
6to5-source (3.3.7)
CFPropertyList (2.2.8)
actionmailer (4.1.8)
actionpack (= 4.1.8)
@ -37,7 +41,6 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.1)
tzinfo (~> 1.1)
addressable (2.3.6)
annotate (2.6.5)
activerecord (>= 2.3.0)
rake (>= 0.8.7)
@ -97,8 +100,7 @@ GEM
rake
rake-compiler
fast_xs (0.8.0)
fastimage (1.6.3)
addressable (~> 2.3, >= 2.3.5)
fastimage_discourse (1.6.6)
ffi (1.9.6)
fission (0.5.0)
CFPropertyList (~> 2.2)
@ -226,7 +228,7 @@ GEM
metaclass (~> 0.0.1)
mock_redis (0.13.2)
moneta (0.8.0)
msgpack (0.5.10)
msgpack (0.5.11)
multi_json (1.10.1)
multi_xml (0.5.5)
multipart-post (2.0.0)
@ -272,7 +274,7 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
onebox (1.5.11)
onebox (1.5.12)
moneta (~> 0.7)
multi_json (~> 1.7)
mustache (~> 0.99)
@ -456,6 +458,7 @@ PLATFORMS
ruby
DEPENDENCIES
6to5
actionpack-action_caching
active_model_serializers (~> 0.8.0)
annotate
@ -472,7 +475,7 @@ DEPENDENCIES
fast_blank
fast_xor
fast_xs
fastimage
fastimage_discourse
flamegraph
fog (= 1.26.0)
foreman

View File

@ -17,6 +17,7 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/nexus-7-mobile-discourse-small3.png)](http://discuss.atom.io)
[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/iphone-5s-mobile-discourse-small4.png)](http://discourse.soylent.me)
Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/).
## Development

View File

@ -0,0 +1,18 @@
export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['active'],
router: function() {
return this.container.lookup('router:main');
}.property(),
active: function() {
const route = this.get('route');
if (!route) { return; }
const routeParam = this.get('routeParam'),
router = this.get('router');
return routeParam ? router.isActive(route, routeParam) : router.isActive(route);
}.property('router.url', 'route')
});

View File

@ -53,6 +53,9 @@ export default Ember.Controller.extend({
actions: {
refreshProblems: function() {
this.loadProblems();
},
showTrafficReport: function() {
this.set("showTrafficReport", true);
}
}

View File

@ -3,12 +3,12 @@ export default Em.ObjectController.extend({
disableSave: false,
currentPage: function() {
if (this.get("user_count") == 0) { return 0; }
if (this.get("user_count") === 0) { return 0; }
return Math.floor(this.get("offset") / this.get("limit")) + 1;
}.property("limit", "offset", "user_count"),
totalPages: function() {
if (this.get("user_count") == 0) { return 0; }
if (this.get("user_count") === 0) { return 0; }
return Math.floor(this.get("user_count") / this.get("limit")) + 1;
}.property("limit", "user_count"),

View File

@ -4,19 +4,19 @@ export default Ember.ArrayController.extend(Discourse.Presence, {
loading: false,
actions: {
clearBlock: function(row){
clearBlock(row){
row.clearBlock().then(function(){
// feeling lazy
window.location.reload();
});
},
exportScreenedEmailList: function(subject) {
exportScreenedEmailList() {
Discourse.ExportCsv.exportScreenedEmailList().then(outputExportResult);
}
},
show: function() {
show() {
var self = this;
self.set('loading', true);
Discourse.ScreenedEmail.findAll().then(function(result) {

View File

@ -3,26 +3,27 @@ import { outputExportResult } from 'discourse/lib/export-result';
export default Ember.ArrayController.extend(Discourse.Presence, {
loading: false,
itemController: 'admin-log-screened-ip-address',
filter: null,
show: function() {
show: Discourse.debounce(function() {
var self = this;
self.set('loading', true);
Discourse.ScreenedIpAddress.findAll().then(function(result) {
Discourse.ScreenedIpAddress.findAll(this.get("filter")).then(function(result) {
self.set('model', result);
self.set('loading', false);
});
},
}, 250).observes("filter"),
actions: {
recordAdded: function(arg) {
recordAdded(arg) {
this.get("model").unshiftObject(arg);
},
rollUp: function() {
var self = this;
rollUp() {
const self = this;
return bootbox.confirm(I18n.t("admin.logs.screened_ips.roll_up_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function (confirmed) {
if (confirmed) {
self.set("loading", true)
self.set("loading", true);
return Discourse.ScreenedIpAddress.rollUp().then(function(results) {
if (results && results.subnets) {
if (results.subnets.length > 0) {
@ -38,7 +39,7 @@ export default Ember.ArrayController.extend(Discourse.Presence, {
});
},
exportScreenedIpList: function(subject) {
exportScreenedIpList() {
Discourse.ExportCsv.exportScreenedIpList().then(outputExportResult);
}
}

View File

@ -3,8 +3,8 @@ import { outputExportResult } from 'discourse/lib/export-result';
export default Ember.ArrayController.extend(Discourse.Presence, {
loading: false,
show: function() {
var self = this;
show() {
const self = this;
self.set('loading', true);
Discourse.ScreenedUrl.findAll().then(function(result) {
self.set('model', result);
@ -13,7 +13,7 @@ export default Ember.ArrayController.extend(Discourse.Presence, {
},
actions: {
exportScreenedUrlList: function(subject) {
exportScreenedUrlList() {
Discourse.ExportCsv.exportScreenedUrlList().then(outputExportResult);
}
}

View File

@ -1,70 +1,97 @@
/**
This controller supports the interface for listing staff action logs in the admin section.
@class AdminLogsStaffActionLogsController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
import { outputExportResult } from 'discourse/lib/export-result';
export default Ember.ArrayController.extend(Discourse.Presence, {
loading: false,
filters: {},
filters: null,
show: function() {
var self = this;
this.set('loading', true);
Discourse.URL.set('queryParams', this.get('filters')); // TODO: doesn't work
Discourse.StaffActionLog.findAll(this.get('filters')).then(function(result) {
self.set('model', result);
self.set('loading', false);
});
}.observes('filters.action_name', 'filters.acting_user', 'filters.target_user', 'filters.subject'),
filtersExists: function() {
return (_.size(this.get('filters')) > 0);
}.property('filters.action_name', 'filters.acting_user', 'filters.target_user', 'filters.subject'),
filtersExists: Ember.computed.gt('filterCount', 0),
actionFilter: function() {
if (this.get('filters.action_name')) {
return I18n.t("admin.logs.staff_actions.actions." + this.get('filters.action_name'));
var name = this.get('filters.action_name');
if (name) {
return I18n.t("admin.logs.staff_actions.actions." + name);
} else {
return null;
}
}.property('filters.action_name'),
showInstructions: function() {
return this.get('model.length') > 0;
}.property('loading', 'model.length'),
showInstructions: Ember.computed.gt('model.length', 0),
refresh: function() {
var self = this;
this.set('loading', true);
var filters = this.get('filters'),
params = {},
count = 0;
// Don't send null values
Object.keys(filters).forEach(function(k) {
var val = filters.get(k);
if (val) {
params[k] = val;
count += 1;
}
});
this.set('filterCount', count);
Discourse.StaffActionLog.findAll(params).then(function(result) {
self.set('model', result);
}).finally(function() {
self.set('loading', false);
});
},
resetFilters: function() {
this.set('filters', Ember.Object.create());
this.refresh();
}.on('init'),
_changeFilters: function(props) {
this.get('filters').setProperties(props);
this.refresh();
},
actions: {
clearFilter: function(key) {
delete this.get('filters')[key];
this.notifyPropertyChange('filters');
var changed = {};
// Special case, clear all action related stuff
if (key === 'actionFilter') {
changed.action_name = null;
changed.action_id = null;
changed.custom_type = null;
} else {
changed[key] = null;
}
this._changeFilters(changed);
},
clearAllFilters: function() {
this.set('filters', {});
this.resetFilters();
},
filterByAction: function(action) {
this.set('filters.action_name', action);
filterByAction: function(logItem) {
this._changeFilters({
action_name: logItem.get('action_name'),
action_id: logItem.get('action'),
custom_type: logItem.get('custom_type')
});
},
filterByStaffUser: function(acting_user) {
this.set('filters.acting_user', acting_user.username);
this._changeFilters({ acting_user: acting_user.username });
},
filterByTargetUser: function(target_user) {
this.set('filters.target_user', target_user.username);
this._changeFilters({ target_user: target_user.username });
},
filterBySubject: function(subject) {
this.set('filters.subject', subject);
this._changeFilters({ subject: subject });
},
exportStaffActionLogs: function(subject) {
exportStaffActionLogs: function() {
Discourse.ExportCsv.exportStaffActionLogs().then(outputExportResult);
}
}

View File

@ -0,0 +1,6 @@
export default Ember.ArrayController.extend({
adminRoutes: function() {
return this.get('model').map(p => p.admin_route).compact();
}.property()
});

View File

@ -98,7 +98,21 @@ Discourse.AdminUser = Discourse.User.extend({
this.set('admin', true);
this.set('can_grant_admin', false);
this.set('can_revoke_admin', true);
Discourse.ajax("/admin/users/" + (this.get('id')) + "/grant_admin", {type: 'PUT'});
var self = this;
Discourse.ajax("/admin/users/" + (this.get('id')) + "/grant_admin", {type: 'PUT'})
.then(null, function(e) {
self.set('admin', false);
self.set('can_grant_admin', true);
self.set('can_revoke_admin', false);
var 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);
});
},
// Revoke the user's moderation access
@ -113,7 +127,20 @@ Discourse.AdminUser = Discourse.User.extend({
this.set('moderator', true);
this.set('can_grant_moderation', false);
this.set('can_revoke_moderation', true);
Discourse.ajax("/admin/users/" + (this.get('id')) + "/grant_moderation", {type: 'PUT'});
var self = this;
Discourse.ajax("/admin/users/" + (this.get('id')) + "/grant_moderation", {type: 'PUT'})
.then(null, function(e) {
self.set('moderator', false);
self.set('can_grant_moderation', true);
self.set('can_revoke_moderation', false);
var 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);
});
},
refreshBrowsers: function() {

View File

@ -45,8 +45,8 @@ Discourse.ScreenedIpAddress = Discourse.Model.extend({
});
Discourse.ScreenedIpAddress.reopenClass({
findAll: function() {
return Discourse.ajax("/admin/logs/screened_ip_addresses.json").then(function(screened_ips) {
findAll: function(filter) {
return Discourse.ajax("/admin/logs/screened_ip_addresses.json", { data: { filter: filter } }).then(function(screened_ips) {
return screened_ips.map(function(b) {
return Discourse.ScreenedIpAddress.create(b);
});

View File

@ -1,11 +1,3 @@
/**
Represents an action taken by a staff member that has been logged.
@class StaffActionLog
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.StaffActionLog = Discourse.Model.extend({
showFullDetails: false,

View File

@ -24,7 +24,8 @@ export default Discourse.Route.extend({
c.set('top_referrers', topReferrers);
}
['admins', 'moderators', 'blocked', 'suspended', 'top_traffic_sources', 'top_referred_topics', 'updated_at'].forEach(function(x) {
[ 'disk_space','admins', 'moderators', 'blocked', 'suspended',
'top_traffic_sources', 'top_referred_topics', 'updated_at'].forEach(function(x) {
c.set(x, d[x]);
});

View File

@ -2,4 +2,4 @@ export default Discourse.Route.extend({
redirect: function() {
this.transitionTo("adminGroupsType", "custom");
}
})
});

View File

@ -1,17 +1,17 @@
export default Discourse.Route.extend({
model: function(params) {
model(params) {
return Discourse.Group.findAll().then(function(groups) {
return groups.filterBy("type", params.type);
});
},
actions: {
newGroup: function() {
var self = this;
newGroup() {
const self = this;
this.transitionTo("adminGroupsType", "custom").then(function() {
var group = Discourse.Group.create({ automatic: false, visible: true });
self.transitionTo("adminGroup", group);
})
});
}
}
});

View File

@ -0,0 +1,23 @@
export default Discourse.Route.extend({
// TODO: make this automatic using an `{{outlet}}`
renderTemplate: function() {
this.render('admin/templates/logs/staff_action_logs', {into: 'adminLogs'});
},
setupController: function(controller) {
controller.resetFilters();
controller.refresh();
},
actions: {
showDetailsModal: function(logRecord) {
Discourse.Route.showModal(this, 'admin_staff_action_log_details', logRecord);
this.controllerFor('modal').set('modalClass', 'log-details-modal');
},
showCustomDetailsModal: function(logRecord) {
Discourse.Route.showModal(this, logRecord.action_name + '_details', logRecord);
this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal');
}
}
});

View File

@ -0,0 +1,12 @@
export default Ember.Route.extend({
model() {
return this.store.findAll('plugin');
},
actions: {
showSettings() {
this.transitionTo('adminSiteSettingsCategory', 'plugins');
}
}
});

View File

@ -1,7 +1,7 @@
export default {
resource: 'admin',
map: function() {
map() {
this.route('dashboard', { path: '/' });
this.resource('adminSiteSettings', { path: '/site_settings' }, function() {
this.resource('adminSiteSettingsCategory', { path: 'category/:category_id'} );

View File

@ -12,47 +12,6 @@ Discourse.AdminLogsIndexRoute = Discourse.Route.extend({
}
});
/**
The route that lists staff actions that were logged.
@class AdminLogsStaffActionLogsRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminLogsStaffActionLogsRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/logs/staff_action_logs', {into: 'adminLogs'});
},
setupController: function(controller) {
var queryParams = Discourse.URL.get('queryParams');
if (queryParams) {
controller.set('filters', queryParams);
}
return controller.show();
},
actions: {
showDetailsModal: function(logRecord) {
Discourse.Route.showModal(this, 'admin_staff_action_log_details', logRecord);
this.controllerFor('modal').set('modalClass', 'log-details-modal');
},
showCustomDetailsModal: function(logRecord) {
Discourse.Route.showModal(this, logRecord.action_name + '_details', logRecord);
this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal');
}
},
deactivate: function() {
this._super();
// Clear any filters when we leave the route
Discourse.URL.set('queryParams', null);
}
});
/**
The route that lists blocked email addresses.

View File

@ -4,25 +4,26 @@
<div class="full-width">
<ul class="nav nav-pills">
<li>{{#link-to 'admin.dashboard'}}{{i18n 'admin.dashboard.title'}}{{/link-to}}</li>
{{admin-nav-item route='admin.dashboard' label='admin.dashboard.title'}}
{{#if currentUser.admin}}
<li>{{#link-to 'adminSiteSettings'}}{{i18n 'admin.site_settings.title'}}{{/link-to}}</li>
{{admin-nav-item route='adminSiteSettings' label='admin.site_settings.title'}}
{{/if}}
<li>{{#link-to 'adminUsersList'}}{{i18n 'admin.users.title'}}{{/link-to}}</li>
{{admin-nav-item route='adminUsersList' label='admin.users.title'}}
{{#if showBadges}}
<li>{{#link-to 'adminBadges.index'}}{{i18n 'admin.badges.title'}}{{/link-to}}</li>
{{admin-nav-item route='adminBadges.index' label='admin.badges.title'}}
{{/if}}
{{#if currentUser.admin}}
<li>{{#link-to 'adminGroups'}}{{i18n 'admin.groups.title'}}{{/link-to}}</li>
{{admin-nav-item route='adminGroups' label='admin.groups.title'}}
{{/if}}
<li>{{#link-to 'adminEmail'}}{{i18n 'admin.email.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminFlags'}}{{i18n 'admin.flags.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminLogs'}}{{i18n 'admin.logs.title'}}{{/link-to}}</li>
{{admin-nav-item route='adminEmail' label='admin.email.title'}}
{{admin-nav-item route='adminFlags' label='admin.flags.title'}}
{{admin-nav-item route='adminLogs' label='admin.logs.title'}}
{{#if currentUser.admin}}
<li>{{#link-to 'adminCustomize.colors'}}{{i18n 'admin.customize.title'}}{{/link-to}}</li>
<li>{{#link-to 'admin.api'}}{{i18n 'admin.api.title'}}{{/link-to}}</li>
<li>{{#link-to 'admin.backups'}}{{i18n 'admin.backups.title'}}{{/link-to}}</li>
{{admin-nav-item route='adminCustomize.colors' label='admin.customize.title'}}
{{admin-nav-item route='admin.api' label='admin.api.title'}}
{{admin-nav-item route='admin.backups' label='admin.backups.title'}}
{{/if}}
{{admin-nav-item route='adminPlugins' label='admin.plugins.title'}}
{{plugin-outlet "admin-menu" tagName="li"}}
</ul>

View File

@ -1,18 +1,31 @@
<div class="admin-controls">
<div class="span15">
<ul class="nav nav-pills">
<li>{{#link-to "admin.backups.index"}}{{i18n 'admin.backups.menu.backups'}}{{/link-to}}</li>
<li>{{#link-to "admin.backups.logs"}}{{i18n 'admin.backups.menu.logs'}}{{/link-to}}</li>
{{admin-nav-item route='admin.backups.index' label='admin.backups.menu.backups'}}
{{admin-nav-item route='admin.backups.logs' label='admin.backups.menu.logs'}}
</ul>
</div>
<div class="pull-right">
{{#if canRollback}}
<button {{action "rollback"}} class="btn btn-rollback" title="{{i18n 'admin.backups.operations.rollback.title'}}" {{bind-attr disabled="rollbackDisabled"}}><i class="fa fa-ambulance fa-flip-horizontal"></i>{{i18n 'admin.backups.operations.rollback.text'}}</button>
{{d-button action="rollback"
class="btn-rollback"
label="admin.backups.operations.rollback.text"
title="admin.backups.operations.rollback.title"
icon="ambulance"
disabled=rollbackDisabled}}
{{/if}}
{{#if isOperationRunning}}
<button {{action "cancelOperation"}} class="btn btn-danger" title="{{i18n 'admin.backups.operations.cancel.title'}}"><i class="fa fa-times"></i>{{i18n 'admin.backups.operations.cancel.text'}}</button>
{{d-button action="cancelOperation"
class="btn-danger"
title="admin.backups.operations.cancel.title"
label="admin.backups.operations.cancel.text"
icon="times"}}
{{else}}
<button {{action "startBackup"}} class="btn btn-primary" title="{{i18n 'admin.backups.operations.backup.title'}}"><i class="fa fa-rocket"></i>{{i18n 'admin.backups.operations.backup.text'}}</button>
{{d-button action="startBackup"
class="btn-primary"
title="admin.backups.operations.backup.title"
label="admin.backups.operations.backup.text"
icon="rocket"}}
{{/if}}
</div>
</div>

View File

@ -0,0 +1,9 @@
{{#if routeParam}}
{{#link-to route routeParam}}{{i18n label}}{{/link-to}}
{{else}}
{{#if route}}
{{#link-to route}}{{i18n label}}{{/link-to}}
{{else}}
<a href="{{unbound href}}" data-auto-route="true">{{i18n label}}</a>
{{/if}}
{{/if}}

View File

@ -0,0 +1,7 @@
<div class='admin-controls'>
<div class='span15'>
<ul class="nav nav-pills">
{{yield}}
</ul>
</div>
</div>

View File

@ -1,14 +1,10 @@
<div class='admin-controls'>
<div class='span15'>
<ul class="nav nav-pills">
<li>{{#link-to 'adminCustomize.colors'}}{{i18n 'admin.customize.colors.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminCustomize.css_html'}}{{i18n 'admin.customize.css_html.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminSiteText'}}{{i18n 'admin.site_text.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminUserFields'}}{{i18n 'admin.user_fields.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminEmojis'}}{{i18n 'admin.emoji.title'}}{{/link-to}}</li>
</ul>
</div>
</div>
{{#admin-nav}}
{{admin-nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
{{admin-nav-item route='adminCustomize.css_html' label='admin.customize.css_html.title'}}
{{admin-nav-item route='adminSiteText' label='admin.site_text.title'}}
{{admin-nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{admin-nav-item route='adminEmojis' label='admin.emoji.title'}}
{{/admin-nav}}
<div class="admin-container">
{{outlet}}

View File

@ -62,6 +62,28 @@
</table>
</div>
<div class="dashboard-stats">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th class="title" title="{{i18n 'admin.dashboard.page_views'}}">{{i18n 'admin.dashboard.page_views_short'}}</th>
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
</tr>
</thead>
{{#unless loading}}
{{ render 'admin_report_counts' page_view_anon_reqs }}
{{ render 'admin_report_counts' page_view_logged_in_reqs }}
{{ render 'admin_report_counts' page_view_crawler_reqs }}
{{ render 'admin_report_counts' page_view_total_reqs }}
{{/unless}}
</table>
</div>
<div class="dashboard-stats">
<table class="table table-condensed table-hover">
<thead>
@ -100,6 +122,59 @@
{{/unless}}
</table>
</div>
<div class="dashboard-stats">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>&nbsp;</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{{#unless loading}}
<tr>
<td>{{i18n 'admin.dashboard.uploads'}}</td>
<td>{{disk_space.uploads_used}} ({{i18n 'admin.dashboard.space_free' size=disk_space.uploads_free}})</td>
<td>{{i18n 'admin.dashboard.backups'}}</td>
<td>{{disk_space.backups_used}} ({{i18n 'admin.dashboard.space_free' size=disk_space.backups_free}})</td>
</tr>
{{/unless}}
</tbody>
</table>
</div>
{{#unless loading}}
{{#if showTrafficReport}}
<div class="dashboard-stats">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th class="title" title="{{i18n 'admin.dashboard.traffic'}}">{{i18n 'admin.dashboard.traffic_short'}}</th>
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
<th>{{i18n 'admin.dashboard.reports.last_30_days'}}</th>
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
</tr>
</thead>
{{#unless loading}}
{{ render 'admin_report_counts' http_2xx_reqs }}
{{ render 'admin_report_counts' http_3xx_reqs}}
{{ render 'admin_report_counts' http_4xx_reqs}}
{{ render 'admin_report_counts' http_5xx_reqs}}
{{ render 'admin_report_counts' http_background_reqs }}
{{ render 'admin_report_counts' http_total_reqs }}
{{/unless}}
</table>
</div>
{{else}}
<div class="dashboard-stats">
<a href {{action showTrafficReport}}>{{i18n 'admin.dashboard.show_traffic_report'}}</a>
</div>
{{/if}}
{{/unless}}
</div>
<div class="dashboard-right">

View File

@ -1,14 +1,10 @@
<div class='admin-controls'>
<div class='span15'>
<ul class="nav nav-pills">
<li>{{#link-to 'adminEmail.index'}}{{i18n 'admin.email.settings'}}{{/link-to}}</li>
<li>{{#link-to 'adminEmail.all'}}{{i18n 'admin.email.all'}}{{/link-to}}</li>
<li>{{#link-to 'adminEmail.sent'}}{{i18n 'admin.email.sent'}}{{/link-to}}</li>
<li>{{#link-to 'adminEmail.skipped'}}{{i18n 'admin.email.skipped'}}{{/link-to}}</li>
<li>{{#link-to 'adminEmail.previewDigest'}}{{i18n 'admin.email.preview_digest'}}{{/link-to}}</li>
</ul>
</div>
</div>
{{#admin-nav}}
{{admin-nav-item route='adminEmail.index' label='admin.email.settings'}}
{{admin-nav-item route='adminEmail.all' label='admin.email.all'}}
{{admin-nav-item route='adminEmail.sent' label='admin.email.sent'}}
{{admin-nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
{{admin-nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
{{/admin-nav}}
<div class="admin-container">
{{outlet}}

View File

@ -1,11 +1,7 @@
<div class='admin-controls'>
<div class='span15'>
<ul class="nav nav-pills">
<li>{{#link-to 'adminFlags.list' 'active'}}{{i18n 'admin.flags.active'}}{{/link-to}}</li>
<li>{{#link-to 'adminFlags.list' 'old'}}{{i18n 'admin.flags.old'}}{{/link-to}}</li>
</ul>
</div>
</div>
{{#admin-nav}}
{{admin-nav-item route='adminFlags.list' routeParam='active' label='admin.flags.active'}}
{{admin-nav-item route='adminFlags.list' routeParam='old' label='admin.flags.old'}}
{{/admin-nav}}
<div class="admin-container">
{{outlet}}

View File

@ -1,11 +1,8 @@
<div class="admin-controls">
<div class="span15">
<ul class="nav nav-pills">
<li>{{#link-to "adminGroupsType" "custom"}}{{i18n 'admin.groups.custom'}}{{/link-to}}</li>
<li>{{#link-to "adminGroupsType" "automatic"}}{{i18n 'admin.groups.automatic'}}{{/link-to}}</li>
</ul>
</div>
</div>
{{#admin-nav}}
{{admin-nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}}
{{admin-nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}}
{{/admin-nav}}
<div class="admin-container">
{{outlet}}
</div>

View File

@ -1,14 +1,12 @@
<div class='admin-controls'>
<div class='span15'>
<ul class="nav nav-pills">
<li>{{#link-to 'adminLogs.staffActionLogs'}}{{i18n 'admin.logs.staff_actions.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminLogs.screenedEmails'}}{{i18n 'admin.logs.screened_emails.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminLogs.screenedIpAddresses'}}{{i18n 'admin.logs.screened_ips.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminLogs.screenedUrls'}}{{i18n 'admin.logs.screened_urls.title'}}{{/link-to}}</li>
<li><a href="/logs" data-auto-route="true">{{i18n 'admin.logs.logster.title'}}</a></li>
</ul>
</div>
</div>
{{#admin-nav}}
{{admin-nav-item route='adminLogs.staffActionLogs' label='admin.logs.staff_actions.title'}}
{{admin-nav-item route='adminLogs.screenedEmails' label='admin.logs.screened_emails.title'}}
{{admin-nav-item route='adminLogs.screenedIpAddresses' label='admin.logs.screened_ips.title'}}
{{admin-nav-item route='adminLogs.screenedUrls' label='admin.logs.screened_urls.title'}}
{{#if currentUser.admin}}
{{admin-nav-item href='/logs' label='admin.logs.logster.title'}}
{{/if}}
{{/admin-nav}}
<div class="admin-container">
{{outlet}}

View File

@ -1,5 +1,6 @@
<p>{{i18n 'admin.logs.screened_ips.description'}}</p>
<div class="pull-right">
{{text-field value=filter class="ip-address-input" placeholderKey="admin.logs.screened_ips.form.filter" autocorrect="off" autocapitalize="off"}}
<button class="btn" {{action "rollUp"}} title="{{i18n 'admin.logs.screened_ips.roll_up.title'}}">{{i18n 'admin.logs.screened_ips.roll_up.text'}}</button>
<button class="btn" {{action "exportScreenedIpList"}} title="{{i18n 'admin.export_csv.button_title.screened_ip'}}">{{fa-icon "download"}}{{i18n 'admin.export_csv.button_text'}}</button>
</div>

View File

@ -3,33 +3,33 @@
<span class="label">{{i18n 'admin.logs.staff_actions.clear_filters'}}</span>
</a>
{{#if actionFilter}}
<a {{action "clearFilter" "action_name"}} class="filter">
<a {{action "clearFilter" "actionFilter"}} class="filter">
<span class="label">{{i18n 'admin.logs.action'}}</span>: {{actionFilter}}
<i class="fa fa-times-circle"></i>
{{fa-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.acting_user}}
<a {{action "clearFilter" "acting_user"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.staff_user'}}</span>: {{filters.acting_user}}
<i class="fa fa-times-circle"></i>
{{fa-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.target_user}}
<a {{action "clearFilter" "target_user"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.target_user'}}</span>: {{filters.target_user}}
<i class="fa fa-times-circle"></i>
{{fa-icon "times-circle"}}
</a>
{{/if}}
{{#if filters.subject}}
<a {{action "clearFilter" "subject"}} class="filter">
<span class="label">{{i18n 'admin.logs.staff_actions.subject'}}</span>: {{filters.subject}}
<i class="fa fa-times-circle"></i>
{{fa-icon "times-circle"}}
</a>
{{/if}}
</div>
<div class="pull-right">
<button class="btn" {{action "exportStaffActionLogs"}} title="{{i18n 'admin.export_csv.button_title.staff_action'}}">{{fa-icon "download"}}{{i18n 'admin.export_csv.button_text'}}</button>
{{d-button action="exportStaffActionLogs" label="admin.export_csv.button_text" icon="download"}}
</div>
</br>

View File

@ -3,7 +3,7 @@
<a {{action "filterByStaffUser" acting_user}} class="btn btn-small">{{acting_user.username}}</a>
</div>
<div class="col value action">
<a {{action "filterByAction" action_name}} class="btn btn-small">{{actionName}}</a>
<a {{action "filterByAction" this}} class="btn btn-small">{{actionName}}</a>
</div>
<div class="col value subject">
{{#if target_user}}

View File

@ -0,0 +1,39 @@
{{#if length}}
{{d-button label="admin.plugins.change_settings"
icon="gear"
class='settings-button pull-right'
action="showSettings"}}
<h3>{{i18n "admin.plugins.installed"}}</h3>
<br/>
<table>
<tbody>
<thead>
<tr>
<th>{{i18n "admin.plugins.name"}}</th>
<th>{{i18n "admin.plugins.version"}}</th>
</tr>
</thead>
{{#each plugin in controller}}
<tr>
<td>
{{#if plugin.url}}
<a {{bind-attr href=plugin.url}} target="_blank">{{plugin.name}}</a>
{{else}}
{{plugin.name}}
{{/if}}
</td>
<td>{{plugin.version}}</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<p>{{i18n "admin.plugins.none_installed"}}</p>
{{/if}}
<p><a href="https://meta.discourse.org/t/install-a-plugin/19157">{{i18n "admin.plugins.howto"}}</a></p>

View File

@ -0,0 +1,13 @@
<div class="admin-nav pull-left">
<ul class="nav nav-stacked">
{{admin-nav-item route='adminPlugins.index' label="admin.plugins.title"}}
{{#each route in adminRoutes}}
{{admin-nav-item route=route.full_location label=route.label}}
{{/each}}
</ul>
</div>
<div class="admin-detail pull-left">
{{outlet}}
</div>

View File

@ -11,7 +11,7 @@
</div>
</div>
<div class="site-settings-nav pull-left">
<div class="admin-nav pull-left">
<ul class="nav nav-stacked">
{{#each category in controller}}
{{#link-to 'adminSiteSettingsCategory' category.nameKey tagName='li' class=category.nameKey}}
@ -26,7 +26,7 @@
</ul>
</div>
<div class="site-settings-detail pull-left">
<div class="admin-detail pull-left">
{{outlet}}
</div>

View File

@ -1,19 +1,19 @@
<div class='admin-controls'>
<div class='span15'>
<ul class="nav nav-pills">
<li>{{#link-to 'adminUsersList.show' 'active'}}{{i18n 'admin.users.nav.active'}}{{/link-to}}</li>
<li>{{#link-to 'adminUsersList.show' 'new'}}{{i18n 'admin.users.nav.new'}}{{/link-to}}</li>
{{admin-nav-item route='adminUsersList.show' routeParam='active' label='admin.users.nav.active'}}
{{admin-nav-item route='adminUsersList.show' routeParam='new' label='admin.users.nav.new'}}
{{#if siteSettings.must_approve_users}}
<li>{{#link-to 'adminUsersList.show' 'pending'}}{{i18n 'admin.users.nav.pending'}}{{/link-to}}</li>
{{admin-nav-item route='adminUsersList.show' routeParam='pending' label='admin.users.nav.pending'}}
{{/if}}
<li>{{#link-to 'adminUsersList.show' 'staff'}}{{i18n 'admin.users.nav.staff'}}{{/link-to}}</li>
<li>{{#link-to 'adminUsersList.show' 'suspended'}}{{i18n 'admin.users.nav.suspended'}}{{/link-to}}</li>
<li>{{#link-to 'adminUsersList.show' 'blocked'}}{{i18n 'admin.users.nav.blocked'}}{{/link-to}}</li>
<li>{{#link-to 'adminUsersList.show' 'suspect'}}{{i18n 'admin.users.nav.suspect'}}{{/link-to}}</li>
{{admin-nav-item route='adminUsersList.show' routeParam='staff' label='admin.users.nav.staff'}}
{{admin-nav-item route='adminUsersList.show' routeParam='suspended' label='admin.users.nav.suspended'}}
{{admin-nav-item route='adminUsersList.show' routeParam='blocked' label='admin.users.nav.blocked'}}
{{admin-nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}}
</ul>
</div>
<div class="pull-right">
<button {{action "exportUsers"}} class="btn" title="{{i18n 'admin.export_csv.button_title.user'}}"><i class="fa fa-download"></i>{{i18n 'admin.export_csv.button_text'}}</button>
{{d-button action="exportUsers" title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}}
</div>
</div>

View File

@ -9,7 +9,7 @@ export default Discourse.View.extend({
this.setProperties({ formattedLogs: "", index: 0 });
},
_updateFormattedLogs: function() {
_updateFormattedLogs: Discourse.debounce(function() {
var logs = this.get("controller.model");
if (logs.length === 0) {
this._reset(); // reset the cached logs whenever the model is reset
@ -17,7 +17,7 @@ export default Discourse.View.extend({
// 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 = moment(logs[i].get("timestamp")).format("YYYY-MM-DD HH:mm:ss"),
var date = logs[i].get("timestamp"),
message = Handlebars.Utils.escapeExpression(logs[i].get("message"));
formattedLogs += "[" + date + "] " + message + "\n";
}
@ -26,7 +26,7 @@ export default Discourse.View.extend({
// force rerender
this.rerender();
}
}.observes("controller.model.@each"),
}, 150).observes("controller.model.@each"),
render: function(buffer) {
var formattedLogs = this.get("formattedLogs");

View File

@ -1,16 +1,9 @@
/*global Favcount:true*/
/**
The main Discourse Application
@class Discourse
@extends Ember.Application
**/
var DiscourseResolver = require('discourse/ember/resolver').default;
window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
rootElement: '#main',
_docTitle: null,
_docTitle: document.title,
getURL: function(url) {
// If it's a non relative URL, return it.
@ -46,13 +39,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
title = "(" + notifyCount + ") " + title;
}
if (title !== document.title) {
// chrome bug workaround see: http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome
window.setTimeout(function() {
document.title = ".";
document.title = title;
}, 200);
}
document.title = title;
}.observes('_docTitle', 'hasFocus', 'notifyCount'),
faviconChanged: function() {

View File

@ -0,0 +1,46 @@
const ADMIN_MODELS = ['plugin'];
function plural(type) {
return type + 's';
}
function pathFor(type) {
const path = "/" + plural(type);
if (ADMIN_MODELS.indexOf(type) !== -1) {
return "/admin/" + path;
}
return path;
}
const _identityMap = {};
export default Ember.Object.extend({
findAll(type) {
var self = this;
return Discourse.ajax(pathFor(type)).then(function(result) {
return result[plural(type)].map(obj => self._hydrate(type, obj));
});
},
_hydrate(type, obj) {
if (!obj) { throw "Can't hydrate " + type + " of `null`"; }
if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; }
_identityMap[type] = _identityMap[type] || {};
const existing = _identityMap[type][obj.id];
if (existing) {
delete obj.id;
existing.setProperties(obj);
return existing;
}
const klass = this.container.lookupFactory('model:' + type) || Ember.Object;
const model = klass.create(obj);
_identityMap[type][obj.id] = model;
return model;
}
});

View File

@ -28,7 +28,7 @@ export default Ember.Component.extend({
// only # of hours in limited mode
return t.match(/^(\d+\.)?\d+$/);
} else {
if (t.match(/^\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{2}(\s?[AP]M)?$/i)) {
if (t.match(/^\d{4}-\d{1,2}-\d{1,2}(?: \d{1,2}:\d{2}(\s?[AP]M)?){0,1}$/i)) {
// timestamp must be in the future
return moment(t).isAfter();
} else {

View File

@ -41,6 +41,22 @@ export default Ember.Component.extend({
return result;
}.property('category'),
categoryColor: function() {
var category = this.get('category');
if (category) {
var color = get(category, 'color');
if (color) {
var style = "";
if (color) { style += "background-color: #" + color + ";" }
return style;
}
}
return "background-color: #eee;";
}.property('category'),
badgeStyle: function() {
var category = this.get('category');

View File

@ -6,26 +6,31 @@ export default Ember.Component.extend({
attributeBindings: ['disabled', 'translatedTitle:title'],
translatedTitle: function() {
var label = this.get('label');
const title = this.get('title');
return title ? I18n.t(title) : this.get('translatedLabel');
}.property('title', 'translatedLabel'),
translatedLabel: function() {
const label = this.get('label');
if (label) {
return I18n.t(this.get('label'));
}
}.property('label'),
render: function(buffer) {
var title = this.get('translatedTitle'),
icon = this.get('icon');
render(buffer) {
const label = this.get('translatedLabel'),
icon = this.get('icon');
if (title || icon) {
if (label || icon) {
if (icon) { buffer.push(iconHTML(icon) + ' '); }
if (title) { buffer.push(title); }
if (label) { buffer.push(label); }
} else {
// If no label or icon is present, yield
return this._super();
}
},
click: function() {
click() {
this.sendAction("action", this.get("actionParam"));
}
});

View File

@ -3,16 +3,11 @@ import TextField from 'discourse/components/text-field';
/**
Same as text-field, but with special features for a password input.
Be sure to test on a variety of browsers and operating systems when changing this logic.
@class PasswordFieldView
@extends Discourse.TextFieldView
@namespace Discourse
@module Discourse
**/
export default TextField.extend({
canToggle: false,
keyPress: function(e) {
keyPress(e) {
if ((e.which >= 65 && e.which <= 90 && !e.shiftKey) || (e.which >= 97 && e.which <= 122 && e.shiftKey)) {
this.set('canToggle', true);
this.set('capsLockOn', true);
@ -22,17 +17,17 @@ export default TextField.extend({
}
},
keyUp: function(e) {
if (e.which == 20 && this.get('canToggle')) {
keyUp(e) {
if (e.which === 20 && this.get('canToggle')) {
this.toggleProperty('capsLockOn');
}
},
focusOut: function(e) {
focusOut() {
this.set('capsLockOn', false);
},
focusIn: function() {
focusIn() {
this.set('canToggle', false); // can't know the state of caps lock yet. keyPress will figure it out.
}
});

View File

@ -1,11 +1,3 @@
/**
The controls for toggling the summarized view on/off
@class ToggleSummaryComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
export default Ember.Component.extend({
layoutName: 'components/toggle-summary',
tagName: 'section',
@ -13,7 +5,7 @@ export default Ember.Component.extend({
postStream: Em.computed.alias('topic.postStream'),
actions: {
toggleSummary: function() {
toggleSummary() {
this.get('postStream').toggleSummary();
}
}

View File

@ -62,7 +62,7 @@ export default DiscourseController.extend({
this.set('model.loading', true);
var composer = this;
return Discourse.Post.load(postId).then(function(post) {
var quote = Discourse.Quote.build(post, post.get("raw"))
var quote = Discourse.Quote.build(post, post.get("raw"));
composer.appendBlockAtCursor(quote);
composer.set('model.loading', false);
});
@ -181,11 +181,11 @@ export default DiscourseController.extend({
// 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')) {
if (!force && composer.get('replyingToTopic')) {
var topic = this.get('topic');
if (!topic || topic.get('id') !== composer.get('topic.id'))
{
var message = I18n.t("composer.posting_not_on_topic", {title: this.get('model.topic.title')});
var message = I18n.t("composer.posting_not_on_topic");
var buttons = [{
"label": I18n.t("composer.cancel"),
@ -193,11 +193,11 @@ export default DiscourseController.extend({
"link": true
}];
if(topic) {
if (topic) {
buttons.push({
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + topic.get('title') + "</div>",
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + Handlebars.Utils.escapeExpression(topic.get('title')) + "</div>",
"class": "btn btn-reply-here",
"callback": function(){
"callback": function() {
composer.set('topic', topic);
composer.set('post', null);
self.save(true);
@ -206,14 +206,14 @@ export default DiscourseController.extend({
}
buttons.push({
"label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + this.get('model.topic.title') + "</div>",
"label": I18n.t("composer.reply_original") + "<br/><div class='topic-title overflow-ellipsis'>" + Handlebars.Utils.escapeExpression(this.get('model.topic.title')) + "</div>",
"class": "btn-primary btn-reply-on-original",
"callback": function(){
"callback": function() {
self.save(true);
}
});
bootbox.dialog(message, buttons, {"classes": "reply-where-modal"});
bootbox.dialog(message, buttons, { "classes": "reply-where-modal" });
return;
}
}

View File

@ -81,7 +81,7 @@ export default ObjectController.extend(ModalFunctionality, {
if (opts) params = $.extend(params, opts);
this.send('hideModal');
postAction.act(params).then(function(result) {
postAction.act(params).then(function() {
self.send('closeModal');
}, function(errors) {
self.send('closeModal');

View File

@ -1,7 +1,6 @@
/* global assetPath:true */
import DiscourseController from 'discourse/controllers/controller';
/*global assetPath:true */
export default DiscourseController.extend({
needs: ['topic', 'composer'],
@ -55,7 +54,7 @@ export default DiscourseController.extend({
// create a marker element
var markerElement = document.createElement("span");
// containing a single invisible character
markerElement.appendChild(document.createTextNode("\ufeff"));
markerElement.appendChild(document.createTextNode("\u{feff}"));
// collapse the range at the beginning/end of the selection
range.collapse(!Discourse.Mobile.isMobileDevice);

View File

@ -26,6 +26,6 @@ export default Ember.Controller.extend({
},
sources: function() {
return Sharing.activeSources();
return Sharing.activeSources(this.siteSettings.share_links);
}.property()
});

View File

@ -1,7 +1,7 @@
import ObjectController from 'discourse/controllers/object';
// Handles displaying of a topic as a list item
export default Ember.ObjectController.extend({
export default ObjectController.extend({
needs: ['discovery/topics'],
canStar: Em.computed.alias('controllers.discovery/topics.currentUser.id'),

View File

@ -1,6 +1,7 @@
import ObjectController from 'discourse/controllers/object';
import BufferedContent from 'discourse/mixins/buffered-content';
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
import Topic from 'discourse/models/topic';
export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedContent, {
multiSelect: false,
@ -272,7 +273,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
var self = this,
props = this.get('buffered.buffer');
Discourse.Topic.update(this.get('model'), props).then(function() {
Topic.update(this.get('model'), props).then(function() {
// Note we roll back on success here because `update` saves
// the properties to the topic.
self.rollbackBuffer();
@ -555,13 +556,13 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
},
// Receive notifications for this topic
subscribe: function() {
subscribe() {
// Unsubscribe before subscribing again
this.unsubscribe();
var topicController = this;
const self = this;
Discourse.MessageBus.subscribe("/topic/" + this.get('id'), function(data) {
var topic = topicController.get('model');
const topic = self.get('model');
if (data.notification_level_change) {
topic.set('details.notification_level', data.notification_level_change);
@ -569,7 +570,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
return;
}
var postStream = topicController.get('postStream');
const postStream = self.get('postStream');
switch (data.type) {
case "revised":
case "acted":
@ -653,12 +654,7 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
}
},
/**
Called the the topmost visible post on the page changes.
@method topVisibleChanged
@params {Discourse.Post} post that is at the top
**/
// Called the the topmost visible post on the page changes.
topVisibleChanged: function(post) {
if (!post) { return; }

View File

@ -2,6 +2,6 @@ export default Ember.ObjectController.extend({
needs: ["application"],
_showFooter: function() {
this.set("controllers.application.showFooter", !this.get("canLoadMore"))
this.set("controllers.application.showFooter", !this.get("canLoadMore"));
}.observes("canLoadMore")
});

View File

@ -3,7 +3,7 @@
a hrefs for them.
**/
var urlReplacerArgs = {
matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/,
spaceOrTagBoundary: true,
emitter: function(matches) {

View File

@ -24,7 +24,7 @@ function loadingResolver(cb) {
function parseName(fullName) {
/*jshint validthis:true */
var nameParts = fullName.split(":"),
const nameParts = fullName.split(":"),
type = nameParts[0], fullNameWithoutType = nameParts[1],
name = fullNameWithoutType,
namespace = get(this, 'namespace'),
@ -44,7 +44,7 @@ export default Ember.DefaultResolver.extend({
parseName: parseName,
normalize: function(fullName) {
normalize(fullName) {
var split = fullName.split(':');
if (split.length > 1) {
var discourseBase = 'discourse/' + split[0] + 's/';
@ -68,14 +68,14 @@ export default Ember.DefaultResolver.extend({
return this._super(fullName);
},
customResolve: function(parsedName) {
customResolve(parsedName) {
// If we end with the name we want, use it. This allows us to define components within plugins.
var suffix = parsedName.type + 's/' + parsedName.fullNameWithoutType,
dashed = Ember.String.dasherize(suffix),
moduleName = Ember.keys(requirejs.entries).find(function(e) {
return (e.indexOf(suffix, e.length - suffix.length) !== -1) ||
(e.indexOf(dashed, e.length - dashed.length) !== -1);
});
const suffix = parsedName.type + 's/' + parsedName.fullNameWithoutType,
dashed = Ember.String.dasherize(suffix),
moduleName = Ember.keys(requirejs.entries).find(function(e) {
return (e.indexOf(suffix, e.length - suffix.length) !== -1) ||
(e.indexOf(dashed, e.length - dashed.length) !== -1);
});
var module;
if (moduleName) {
@ -85,27 +85,31 @@ export default Ember.DefaultResolver.extend({
return module;
},
resolveView: function(parsedName) {
resolveAdapter(parsedName) {
return this.customResolve(parsedName) || this._super(parsedName);
},
resolveView(parsedName) {
return this.findLoadingView(parsedName) || this.customResolve(parsedName) || this._super(parsedName);
},
resolveHelper: function(parsedName) {
resolveHelper(parsedName) {
return this.customResolve(parsedName) || this._super(parsedName);
},
resolveController: function(parsedName) {
resolveController(parsedName) {
return this.customResolve(parsedName) || this._super(parsedName);
},
resolveComponent: function(parsedName) {
resolveComponent(parsedName) {
return this.customResolve(parsedName) || this._super(parsedName);
},
resolveRoute: function(parsedName) {
resolveRoute(parsedName) {
return this.findLoadingRoute(parsedName) || this.customResolve(parsedName) || this._super(parsedName);
},
resolveTemplate: function(parsedName) {
resolveTemplate(parsedName) {
return this.findPluginTemplate(parsedName) ||
this.findMobileTemplate(parsedName) ||
this.findTemplate(parsedName) ||
@ -125,23 +129,23 @@ export default Ember.DefaultResolver.extend({
return _loadingView;
}),
findPluginTemplate: function(parsedName) {
findPluginTemplate(parsedName) {
var pluginParsedName = this.parseName(parsedName.fullName.replace("template:", "template:javascripts/"));
return this.findTemplate(pluginParsedName);
},
findMobileTemplate: function(parsedName) {
findMobileTemplate(parsedName) {
if (Discourse.Mobile.mobileView) {
var mobileParsedName = this.parseName(parsedName.fullName.replace("template:", "template:mobile/"));
return this.findTemplate(mobileParsedName);
}
},
findTemplate: function(parsedName) {
var withoutType = parsedName.fullNameWithoutType,
slashedType = withoutType.replace(/\./g, '/'),
decamelized = withoutType.decamelize(),
templates = Ember.TEMPLATES;
findTemplate(parsedName) {
const withoutType = parsedName.fullNameWithoutType,
slashedType = withoutType.replace(/\./g, '/'),
decamelized = withoutType.decamelize(),
templates = Ember.TEMPLATES;
return this._super(parsedName) ||
templates[slashedType] ||
@ -152,7 +156,7 @@ export default Ember.DefaultResolver.extend({
this.findUnderscoredTemplate(parsedName);
},
findUnderscoredTemplate: function(parsedName) {
findUnderscoredTemplate(parsedName) {
var decamelized = parsedName.fullNameWithoutType.decamelize();
var underscored = decamelized.replace(/\-/g, "_");
return Ember.TEMPLATES[underscored];
@ -160,14 +164,22 @@ export default Ember.DefaultResolver.extend({
// Try to find a template within a special admin namespace, e.g. adminEmail => admin/templates/email
// (similar to how discourse lays out templates)
findAdminTemplate: function(parsedName) {
findAdminTemplate(parsedName) {
var decamelized = parsedName.fullNameWithoutType.decamelize();
if (decamelized.indexOf('admin') === 0) {
if (decamelized.indexOf('components') === 0) {
const compTemplate = Ember.TEMPLATES['admin/templates/' + decamelized];
if (compTemplate) { return compTemplate; }
}
if (decamelized.indexOf('admin') === 0 || decamelized.indexOf('javascripts/admin') === 0) {
decamelized = decamelized.replace(/^admin\_/, 'admin/templates/');
decamelized = decamelized.replace(/^admin\./, 'admin/templates/');
decamelized = decamelized.replace(/\./g, '_');
var dashed = decamelized.replace(/_/g, '-');
return Ember.TEMPLATES[decamelized] || Ember.TEMPLATES[dashed];
const dashed = decamelized.replace(/_/g, '-');
return Ember.TEMPLATES[decamelized] ||
Ember.TEMPLATES[dashed] ||
Ember.TEMPLATES[dashed.replace('admin-', 'admin/')];
}
}

View File

@ -1,6 +1,6 @@
import registerUnbound from 'discourse/helpers/register-unbound';
export function daysSinceEpoch(dt) {
function daysSinceEpoch(dt) {
// 1000 * 60 * 60 * 24 = days since epoch
return dt.getTime() / 86400000;
}
@ -22,3 +22,5 @@ registerUnbound('cold-age-class', function(dt, params) {
return className;
});
export { daysSinceEpoch };

View File

@ -1,12 +1,12 @@
import registerUnbound from 'discourse/helpers/register-unbound';
export function iconClasses(icon, modifier) {
function iconClasses(icon, modifier) {
var classes = "fa fa-" + icon;
if (modifier) { classes += " fa-" + modifier; }
return classes;
}
export function iconHTML(icon, label, modifier) {
function iconHTML(icon, label, modifier) {
var html = "<i class='" + iconClasses(icon, modifier) + "'";
if (label) { html += " aria-hidden='true'"; }
html += "></i>";
@ -20,3 +20,5 @@ export function iconHTML(icon, label, modifier) {
registerUnbound('fa-icon', function(icon, params) {
return new Handlebars.SafeString(iconHTML(icon, params.label, params.modifier));
});
export { iconClasses, iconHTML };

View File

@ -39,18 +39,35 @@
Nobody says hello :'(
{{/plugin-outlet}}
```
## Disabling
If a plugin returns a disabled status, the outlets will not be wired up for it.
The list of disabled plugins is returned via the `Site` singleton.
**/
var _connectorCache;
function findOutlets(collection, callback) {
Ember.keys(collection).forEach(function(i) {
if (i.indexOf("/connectors/") !== -1) {
var segments = i.split("/"),
var disabledPlugins = Discourse.Site.currentProp('disabled_plugins') || [];
Ember.keys(collection).forEach(function(res) {
if (res.indexOf("/connectors/") !== -1) {
// Skip any disabled plugins
for (var i=0; i<disabledPlugins.length; i++) {
if (res.indexOf("/" + disabledPlugins[i] + "/") !== -1) {
return;
}
}
var segments = res.split("/"),
outletName = segments[segments.length-2],
uniqueName = segments[segments.length-1];
callback(outletName, i, uniqueName);
callback(outletName, res, uniqueName);
}
});
}
@ -59,18 +76,18 @@ function buildConnectorCache() {
_connectorCache = {};
var uniqueViews = {};
findOutlets(requirejs._eak_seen, function(outletName, idx, uniqueName) {
findOutlets(requirejs._eak_seen, function(outletName, resource, uniqueName) {
_connectorCache[outletName] = _connectorCache[outletName] || [];
var viewClass = require(idx, null, null, true).default;
var viewClass = require(resource, null, null, true).default;
uniqueViews[uniqueName] = viewClass;
_connectorCache[outletName].pushObject(viewClass);
});
findOutlets(Ember.TEMPLATES, function(outletName, idx, uniqueName) {
findOutlets(Ember.TEMPLATES, function(outletName, resource, uniqueName) {
_connectorCache[outletName] = _connectorCache[outletName] || [];
var mixin = {templateName: idx.replace('javascripts/', '')},
var mixin = {templateName: resource.replace('javascripts/', '')},
viewClass = uniqueViews[uniqueName];
if (viewClass) {
@ -81,7 +98,6 @@ function buildConnectorCache() {
}
_connectorCache[outletName].pushObject(viewClass.extend(mixin));
});
}
export default function(connectionName, options) {

View File

@ -1,7 +1,7 @@
import registerUnbound from 'discourse/helpers/register-unbound';
export function renderRaw(template, templateName, params) {
params.parent = params.parent || this;
function renderRaw(ctx, template, templateName, params) {
params.parent = params.parent || ctx;
if (!params.view) {
var viewClass = Discourse.__container__.lookupFactory('view:' + templateName);
@ -20,5 +20,5 @@ registerUnbound('raw', function(templateName, params) {
return;
}
return renderRaw.call(this, template, templateName, params);
return renderRaw(this, template, templateName, params);
});

View File

@ -1,18 +1,17 @@
var get = Discourse.EmberCompatHandlebars.get;
const get = Discourse.EmberCompatHandlebars.get;
export function resolveParams(options) {
var params = {},
hash = options.hash;
function resolveParams(ctx, options) {
let params = {};
const hash = options.hash;
if (hash) {
var self = this;
if (options.hashTypes) {
Ember.keys(hash).forEach(function(k) {
var type = options.hashTypes[k];
const type = options.hashTypes[k];
if (type === "STRING") {
params[k] = hash[k];
} else if (type === "ID") {
params[k] = get(self, hash[k], options);
params[k] = get(ctx, hash[k], options);
}
});
} else {
@ -24,12 +23,10 @@ export function resolveParams(options) {
export default function registerUnbound(name, fn) {
Handlebars.registerHelper(name, function(property, options) {
if (options.types && options.types[0] === "ID") {
property = get(this, property, options);
}
var params = resolveParams.call(this, options);
return fn.apply(this,[property, params]);
return fn.call(this, property, resolveParams(this, options));
});
}

View File

@ -1,6 +1,6 @@
import registerUnbound from 'discourse/helpers/register-unbound';
export function renderAvatar(user, options) {
function renderAvatar(user, options) {
options = options || {};
if (user) {
@ -44,3 +44,5 @@ export function renderAvatar(user, options) {
registerUnbound('avatar', function(user, params) {
return new Handlebars.SafeString(renderAvatar.call(this, user, params));
});
export { renderAvatar };

View File

@ -1,6 +1,6 @@
import registerUnbound from 'discourse/helpers/register-unbound';
var safe = Handlebars.SafeString;
const Safe = Handlebars.SafeString;
registerUnbound('user-status', function(user) {
if (!user) { return; }
@ -10,11 +10,11 @@ registerUnbound('user-status', function(user) {
if(Discourse.User.currentProp("admin") || Discourse.User.currentProp("moderator")) {
if(user.get('admin')) {
var adminDesc = I18n.t('user.admin', {user: name});
return new safe('<i class="fa fa-shield" title="' + adminDesc + '" alt="' + adminDesc + '"></i>');
return new Safe('<i class="fa fa-shield" title="' + adminDesc + '" alt="' + adminDesc + '"></i>');
}
}
if(user.get('moderator')){
var modDesc = I18n.t('user.moderator', {user: name});
return new safe('<i class="fa fa-shield" title="' + modDesc + '" alt="' + modDesc + '"></i>');
return new Safe('<i class="fa fa-shield" title="' + modDesc + '" alt="' + modDesc + '"></i>');
}
});

View File

@ -6,7 +6,7 @@ export default {
name: 'dynamic-route-builders',
after: 'register-discourse-location',
initialize: function(container, app) {
initialize(container, app) {
app.DiscoveryCategoryRoute = buildCategoryRoute('latest');
app.DiscoveryParentCategoryRoute = buildCategoryRoute('latest');
app.DiscoveryCategoryNoneRoute = buildCategoryRoute('latest', {no_subcategories: true});

View File

@ -1,48 +1,53 @@
import Session from 'discourse/models/session';
import AppEvents from 'discourse/lib/app-events';
import Store from 'discourse/models/store';
export default {
name: "inject-objects",
initialize: function(container, application) {
initialize(container, app) {
// Inject appEvents everywhere
var appEvents = AppEvents.create();
application.register('app-events:main', appEvents, { instantiate: false });
const appEvents = AppEvents.create();
app.register('app-events:main', appEvents, { instantiate: false });
application.inject('controller', 'appEvents', 'app-events:main');
application.inject('component', 'appEvents', 'app-events:main');
application.inject('route', 'appEvents', 'app-events:main');
application.inject('view', 'appEvents', 'app-events:main');
application.inject('model', 'appEvents', 'app-events:main');
app.inject('controller', 'appEvents', 'app-events:main');
app.inject('component', 'appEvents', 'app-events:main');
app.inject('route', 'appEvents', 'app-events:main');
app.inject('view', 'appEvents', 'app-events:main');
app.inject('model', 'appEvents', 'app-events:main');
Discourse.URL.appEvents = appEvents;
// Inject Discourse.Site to avoid using Discourse.Site.current()
var site = Discourse.Site.current();
application.register('site:main', site, { instantiate: false });
application.inject('controller', 'site', 'site:main');
application.inject('component', 'site', 'site:main');
application.inject('route', 'site', 'site:main');
application.inject('view', 'site', 'site:main');
application.inject('model', 'site', 'site:main');
const site = Discourse.Site.current();
app.register('site:main', site, { instantiate: false });
app.inject('controller', 'site', 'site:main');
app.inject('component', 'site', 'site:main');
app.inject('route', 'site', 'site:main');
app.inject('view', 'site', 'site:main');
app.inject('model', 'site', 'site:main');
// Inject Discourse.SiteSettings to avoid using Discourse.SiteSettings globals
application.register('site-settings:main', Discourse.SiteSettings, { instantiate: false });
application.inject('controller', 'siteSettings', 'site-settings:main');
application.inject('component', 'siteSettings', 'site-settings:main');
application.inject('route', 'siteSettings', 'site-settings:main');
application.inject('view', 'siteSettings', 'site-settings:main');
application.inject('model', 'siteSettings', 'site-settings:main');
app.register('site-settings:main', Discourse.SiteSettings, { instantiate: false });
app.inject('controller', 'siteSettings', 'site-settings:main');
app.inject('component', 'siteSettings', 'site-settings:main');
app.inject('route', 'siteSettings', 'site-settings:main');
app.inject('view', 'siteSettings', 'site-settings:main');
app.inject('model', 'siteSettings', 'site-settings:main');
// Inject Session for transient data
application.register('session:main', Session.current(), { instantiate: false });
application.inject('controller', 'session', 'session:main');
application.inject('component', 'session', 'session:main');
application.inject('route', 'session', 'session:main');
application.inject('view', 'session', 'session:main');
application.inject('model', 'session', 'session:main');
app.register('session:main', Session.current(), { instantiate: false });
app.inject('controller', 'session', 'session:main');
app.inject('component', 'session', 'session:main');
app.inject('route', 'session', 'session:main');
app.inject('view', 'session', 'session:main');
app.inject('model', 'session', 'session:main');
// Inject currentUser. Components only for now to prevent any breakage
application.register('current-user:main', Discourse.User.current(), { instantiate: false });
application.inject('component', 'currentUser', 'current-user:main');
app.register('current-user:main', Discourse.User.current(), { instantiate: false });
app.inject('component', 'currentUser', 'current-user:main');
app.register('store:main', Store);
app.inject('route', 'store', 'store:main');
app.inject('controller', 'store', 'store:main');
}
};

View File

@ -8,7 +8,7 @@ export default {
initialize: function () {
if (!Discourse.MessageBus) { return; }
Discourse.MessageBus.subscribe("/logout", function (user_id) {
Discourse.MessageBus.subscribe("/logout", function () {
var refresher = function() {
var redirect = Discourse.SiteSettings.logout_redirect;
if(redirect.length === 0){
@ -17,7 +17,7 @@ export default {
window.location.href = redirect;
}
};
bootbox.dialog(I18n.t("logout"), {label: I18n.t("refresh"), callback: refresher}, {onEscape: refresher, backdrop: 'static'})
bootbox.dialog(I18n.t("logout"), {label: I18n.t("refresh"), callback: refresher}, {onEscape: refresher, backdrop: 'static'});
});
}
};

View File

@ -5,7 +5,14 @@ export default {
name: "page-tracking",
after: 'register-discourse-location',
initialize: function() {
initialize: function(container) {
// Tell our AJAX system to track a page transition
var router = container.lookup('router:main');
router.on('willTransition', function() {
Discourse.viewTrackingRequired();
});
var pageTracker = Discourse.PageTracker.current();
pageTracker.start();
@ -19,7 +26,6 @@ export default {
return;
}
// Also use Universal Analytics if it is present
if (typeof window.ga !== 'undefined') {
pageTracker.on('change', function(url, title) {

View File

@ -0,0 +1,11 @@
import { decorateCooked } from 'discourse/lib/plugin-api';
import HighlightSyntax from 'discourse/lib/highlight-syntax';
import Lightbox from 'discourse/lib/lightbox';
export default {
name: "post-decorations",
initialize: function(container) {
decorateCooked(container, HighlightSyntax);
decorateCooked(container, Lightbox);
}
};

View File

@ -10,7 +10,7 @@ export default {
var site = container.lookup('site:main');
Discourse.MessageBus.subscribe("/site/read-only", function (enabled) {
site.currentProp('isReadOnly', enabled);
site.set('isReadOnly', enabled);
});
}
};

View File

@ -1,11 +1,10 @@
export default Ember.Object.extend(Ember.Evented);
var id = 1;
function newKey() {
return "_view_app_event_" + (id++);
}
export function createViewListener(eventName, cb) {
function createViewListener(eventName, cb) {
var extension = {};
extension[newKey()] = function() {
this.appEvents.on(eventName, this, cb);
@ -18,6 +17,9 @@ export function createViewListener(eventName, cb) {
return extension;
}
export function listenForViewEvent(viewClass, eventName, cb) {
function listenForViewEvent(viewClass, eventName, cb) {
viewClass.reopen(createViewListener(eventName, cb));
}
export { listenForViewEvent, createViewListener };
export default Ember.Object.extend(Ember.Evented);

View File

@ -270,7 +270,7 @@ export default function(options) {
updateAutoComplete(options.dataSource(""));
}
} else if ((completeStart !== null) && (e.charCode !== 0)) {
caretPosition = Discourse.Utilities.caretPosition(me[0]),
caretPosition = Discourse.Utilities.caretPosition(me[0]);
term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
term += String.fromCharCode(e.charCode);
updateAutoComplete(options.dataSource(term));

View File

@ -112,18 +112,17 @@ Discourse.ClickTrack = {
return false;
}
// restore href
setTimeout(function() {
$link.removeClass('no-href');
$link.attr('href', $link.data('href'));
$link.data('href', null);
}, 50);
// Otherwise, use a custom URL with a redirect
if (Discourse.User.currentProp('external_links_in_new_tab')) {
var win = window.open(trackingUrl, '_blank');
win.focus();
// restore href
setTimeout(function(){
$link.removeClass('no-href');
$link.attr('href', $link.data('href'));
$link.data('href', null);
},50);
} else {
Discourse.URL.redirectTo(trackingUrl);
}

View File

@ -136,7 +136,7 @@ var bindEvents = function(page,offset){
$('.emoji-page a').click(function(){
var title = $(this).attr('title');
trackEmojiUsage(title)
trackEmojiUsage(title);
composerController.appendTextAtCursor(":" + title + ":", {space: true});
closeSelector();
return false;

View File

@ -0,0 +1,10 @@
/*global hljs:true */
export default function highlightSyntax($elem) {
const selector = Discourse.SiteSettings.autohighlight_all_code ? 'pre code' : 'pre code[class]';
$(selector, $elem).each(function(i, e) {
return $LAB.script("/javascripts/highlight.pack.js").wait(function() {
return hljs.highlightBlock(e);
});
});
}

View File

@ -1,54 +0,0 @@
/**
Helper object for lightboxes.
@class Lightbox
@namespace Discourse
@module Discourse
**/
Discourse.Lightbox = {
apply: function($elem) {
$LAB.script("/javascripts/jquery.magnific-popup-min.js").wait(function() {
$("a.lightbox", $elem).each(function(i, e) {
var $e = $(e);
// do not lightbox spoiled images
if ($e.parents(".spoiler").length > 0 || $e.parents(".spoiled").length > 0) { return; }
$e.magnificPopup({
type: "image",
closeOnContentClick: false,
removalDelay: 300,
mainClass: "mfp-zoom-in",
callbacks: {
open: function() {
var wrap = this.wrap,
img = this.currItem.img,
maxHeight = img.css("max-height");
wrap.on("click.pinhandler", "img", function() {
wrap.toggleClass("mfp-force-scrollbars");
img.css("max-height", wrap.hasClass("mfp-force-scrollbars") ? "none" : maxHeight);
});
},
beforeClose: function() {
this.wrap.off("click.pinhandler");
this.wrap.removeClass("mfp-force-scrollbars");
}
},
image: {
titleSrc: function(item) {
var href = item.el.data("download-href") || item.src;
return [
item.el.attr("title"),
$("span.informations", item.el).text().replace('x', '&times;'),
'<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>'
].join(' &middot; ');
}
}
});
});
});
}
};

View File

@ -0,0 +1,45 @@
export default function($elem) {
$("a.lightbox", $elem).each(function(i, e) {
$LAB.script("/javascripts/jquery.magnific-popup-min.js").wait(function() {
var $e = $(e);
// do not lightbox spoiled images
if ($e.parents(".spoiler").length > 0 || $e.parents(".spoiled").length > 0) { return; }
$e.magnificPopup({
type: "image",
closeOnContentClick: false,
removalDelay: 300,
mainClass: "mfp-zoom-in",
callbacks: {
open: function() {
var wrap = this.wrap,
img = this.currItem.img,
maxHeight = img.css("max-height");
wrap.on("click.pinhandler", "img", function() {
wrap.toggleClass("mfp-force-scrollbars");
img.css("max-height", wrap.hasClass("mfp-force-scrollbars") ? "none" : maxHeight);
});
},
beforeClose: function() {
this.wrap.off("click.pinhandler");
this.wrap.removeClass("mfp-force-scrollbars");
}
},
image: {
titleSrc: function(item) {
var href = item.el.data("download-href") || item.src;
return [
item.el.attr("title"),
$("span.informations", item.el).text().replace('x', '&times;'),
'<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>'
].join(' &middot; ');
}
}
});
});
});
}

View File

@ -1,4 +1,6 @@
export default function searchForTerm(term, opts) {
import Topic from 'discourse/models/topic';
function searchForTerm(term, opts) {
if (!opts) opts = {};
// Only include the data we have
@ -22,7 +24,7 @@ export default function searchForTerm(term, opts) {
var topicMap = {};
results.topics = results.topics.map(function(topic){
topic = Discourse.Topic.create(topic);
topic = Topic.create(topic);
topicMap[topic.id] = topic;
return topic;
});
@ -66,3 +68,5 @@ export default function searchForTerm(term, opts) {
return noResults ? null : Em.Object.create(results);
});
}
export default searchForTerm;

View File

@ -24,17 +24,14 @@
```
**/
var _sources = [];
var _sources = {};
export default {
addSource: function (source) {
_sources.push(source);
addSource(source) {
_sources[source.id] = source;
},
activeSources: function() {
var enabled = Discourse.SiteSettings.share_links.split('|');
return _sources.filter(function(s) {
return enabled.indexOf(s.id) !== -1;
});
activeSources(linksSetting) {
return linksSetting.split('|').map(s => _sources[s]).compact();
}
};

View File

@ -1,26 +0,0 @@
/*global hljs:true */
/**
Helper object for syntax highlighting. Uses highlight.js which is loaded on demand.
@class SyntaxHighlighting
@namespace Discourse
@module Discourse
**/
Discourse.SyntaxHighlighting = {
/**
Apply syntax highlighting to a jQuery element
@method apply
@param {jQuery.selector} $elem The element we want to apply our highlighting to
**/
apply: function($elem) {
var selector = Discourse.SiteSettings.autohighlight_all_code ? 'pre code' : 'pre code[class]';
$(selector, $elem).each(function(i, e) {
return $LAB.script("/javascripts/highlight.pack.js").wait(function() {
return hljs.highlightBlock(e);
});
});
}
};

View File

@ -146,16 +146,9 @@ Discourse.Utilities = {
}
},
/**
Validate a list of files to be uploaded
@method validateUploadedFiles
@param {Array} files The list of files we want to upload
**/
validateUploadedFiles: function(files, bypassNewUserRestriction) {
if (!files || files.length === 0) { return false; }
// can only upload one file at a time
if (files.length > 1) {
bootbox.alert(I18n.t('post.errors.too_many_uploads'));
return false;
@ -173,15 +166,6 @@ Discourse.Utilities = {
return Discourse.Utilities.validateUploadedFile(upload, type, bypassNewUserRestriction);
},
/**
Validate a file to be uploaded
@method validateUploadedFile
@param {File} file The file to be uploaded
@param {string} type The type of the upload (image, attachment)
@params {bool} bypassNewUserRestriction
@returns true whenever the upload is valid
**/
validateUploadedFile: function(file, type, bypassNewUserRestriction) {
// check that the uploaded file is authorized
if (!Discourse.Utilities.authorizesAllExtensions() &&

View File

@ -1,8 +1,8 @@
// Mix this in to a view that has a `categorySlug` property to automatically
// Mix this in to a view that has a `categoryFullSlug` property to automatically
// add it to the body as the view is entered / left / model is changed.
// This is used for keeping the `body` style in sync for the background image.
export default {
_enterView: function() { this.get('categorySlug'); }.on('init'),
_enterView: function() { this.get('categoryFullSlug'); }.on('init'),
_removeClasses: function() {
$('body').removeClass(function(idx, css) {
@ -11,13 +11,13 @@ export default {
},
_categoryChanged: function() {
var categorySlug = this.get('categorySlug');
var categoryFullSlug = this.get('categoryFullSlug');
this._removeClasses();
if (categorySlug) {
$('body').addClass('category-' + categorySlug);
if (categoryFullSlug) {
$('body').addClass('category-' + categoryFullSlug);
}
}.observes('categorySlug'),
}.observes('categoryFullSlug'),
_leaveView: function() { this._removeClasses(); }.on('willDestroyElement')
};

View File

@ -7,8 +7,15 @@
@namespace Discourse
@module Discourse
**/
var _trackView = false;
Discourse.Ajax = Em.Mixin.create({
viewTrackingRequired: function() {
_trackView = true;
},
/**
Our own $.ajax method. Makes sure the .then method executes in an Ember runloop
for performance reasons. Also automatically adjusts the URL to support installs
@ -34,22 +41,24 @@ Discourse.Ajax = Em.Mixin.create({
}
if (args.success) {
Ember.Logger.error("DEPRECATION: Discourse.ajax should use promises, received 'success' callback");
throw "Discourse.ajax should use promises, received 'success' callback";
}
if (args.error) {
Ember.Logger.error("DEPRECATION: Discourse.ajax should use promises, received 'error' callback");
throw "DEPRECATION: Discourse.ajax should use promises, received 'error' callback";
}
var performAjax = function(resolve, reject) {
var oldSuccess = args.success;
if (_trackView && (!args.type || args.type === "GET")) {
_trackView = false;
args.headers = { 'Discourse-Track-View': true };
}
args.success = function(xhr) {
Ember.run(null, resolve, xhr);
if (oldSuccess) oldSuccess(xhr);
};
var oldError = args.error;
args.error = function(xhr, textStatus) {
// note: for bad CSRF we don't loop an extra request right away.
// this allows us to eliminate the possibility of having a loop.
if (xhr.status === 403 && xhr.responseText === "['BAD CSRF']") {
@ -63,10 +72,7 @@ Discourse.Ajax = Em.Mixin.create({
xhr.jqTextStatus = textStatus;
xhr.requestedUrl = url;
// TODO is this sequence correct? we are calling catch defined externally before
// the error that was defined inline, it should probably be in reverse
Ember.run(null, reject, xhr);
if (oldError) oldError(xhr);
};
// We default to JSON on GET. If we don't, sometimes if the server doesn't return the proper header

View File

@ -2,30 +2,11 @@
This mixin provides a `currentUser` property that can be used to retrieve information
about the currently logged in user. It is mostly useful to controllers so it can be
exposted to templates.
Outside of templates, code should probably use `Discourse.User.current()` instead of
this property.
@class Discourse.HasCurrentUser
@extends Ember.Mixin
@namespace Discourse
@module HasCurrentUser
**/
Discourse.HasCurrentUser = Em.Mixin.create({
/**
Returns a reference to the currently logged in user.
@method currentUser
@return {Discourse.User} the currently logged in user if present.
*/
currentUser: function() {
return Discourse.User.current();
}.property().volatile()
});

View File

@ -13,4 +13,4 @@ export default Em.Mixin.create({
return true;
}
}
})
});

View File

@ -22,6 +22,15 @@ export default Em.Mixin.create({
pasteZone: $upload
});
$upload.on("fileuploaddrop", function (e, data) {
if (data.files.length > 10) {
bootbox.alert(I18n.t("post.errors.too_many_dragged_and_dropped_files"));
return false;
} else {
return true;
}
});
$upload.on('fileuploadsubmit', function (e, data) {
var isValid = Discourse.Utilities.validateUploadedFiles(data.files, true);
var form = { image_type: self.get('type') };

View File

@ -29,6 +29,10 @@ Discourse.Category = Discourse.Model.extend({
return Discourse.getURL("/c/") + Discourse.Category.slugFor(this);
}.property('name'),
fullSlug: function() {
return this.get("url").slice(3).replace("/", "-");
}.property("url"),
nameLower: function() {
return this.get('name').toLowerCase();
}.property('name'),

View File

@ -532,6 +532,7 @@ Discourse.Composer = Discourse.Model.extend({
});
}
// If we're in a topic, we can append the post instantly.
if (postStream) {
// If it's in reply to another post, increase the reply count

View File

@ -1,5 +1,4 @@
Discourse.PostStream = Em.Object.extend({
const PostStream = Ember.Object.extend({
loading: Em.computed.or('loadingAbove', 'loadingBelow', 'loadingFilter', 'stagingPost'),
notLoading: Em.computed.not('loading'),
filteredPostsCount: Em.computed.alias("stream.length"),
@ -45,15 +44,13 @@ Discourse.PostStream = Em.Object.extend({
/**
Returns a JS Object of current stream filter options. It should match the query
params for the stream.
@property streamFilters
**/
streamFilters: function() {
var result = {};
const result = {};
if (this.get('summary')) { result.filter = "summary"; }
if (this.get('show_deleted')) { result.show_deleted = true; }
var userFilters = this.get('userFilters');
const userFilters = this.get('userFilters');
if (!Em.isEmpty(userFilters)) {
result.username_filters = userFilters.join(",");
}
@ -62,27 +59,25 @@ Discourse.PostStream = Em.Object.extend({
}.property('userFilters.[]', 'summary', 'show_deleted'),
hasNoFilters: function() {
var streamFilters = this.get('streamFilters');
const streamFilters = this.get('streamFilters');
return !(streamFilters && ((streamFilters.filter === 'summary') || streamFilters.username_filters));
}.property('streamFilters.[]', 'topic.posts_count', 'posts.length'),
/**
Returns the window of posts above the current set in the stream, bound to the top of the stream.
This is the collection we'll ask for when scrolling upwards.
@property previousWindow
**/
previousWindow: function() {
// If we can't find the last post loaded, bail
var firstPost = _.first(this.get('posts'));
const firstPost = _.first(this.get('posts'));
if (!firstPost) { return []; }
// Find the index of the last post loaded, if not found, bail
var stream = this.get('stream');
var firstIndex = this.indexOf(firstPost);
const stream = this.get('stream');
const firstIndex = this.indexOf(firstPost);
if (firstIndex === -1) { return []; }
var startIndex = firstIndex - this.get('topic.chunk_size');
let startIndex = firstIndex - this.get('topic.chunk_size');
if (startIndex < 0) { startIndex = 0; }
return stream.slice(startIndex, firstIndex);
@ -91,17 +86,15 @@ Discourse.PostStream = Em.Object.extend({
/**
Returns the window of posts below the current set in the stream, bound by the bottom of the
stream. This is the collection we use when scrolling downwards.
@property nextWindow
**/
nextWindow: function() {
// If we can't find the last post loaded, bail
var lastLoadedPost = this.get('lastLoadedPost');
const lastLoadedPost = this.get('lastLoadedPost');
if (!lastLoadedPost) { return []; }
// Find the index of the last post loaded, if not found, bail
var stream = this.get('stream');
var lastIndex = this.indexOf(lastLoadedPost);
const stream = this.get('stream');
const lastIndex = this.indexOf(lastLoadedPost);
if (lastIndex === -1) { return []; }
if ((lastIndex + 1) >= this.get('highest_post_number')) { return []; }
@ -109,41 +102,26 @@ Discourse.PostStream = Em.Object.extend({
return stream.slice(lastIndex+1, lastIndex + this.get('topic.chunk_size') + 1);
}.property('lastLoadedPost', 'stream.@each'),
/**
Cancel any active filters on the stream.
@method cancelFilter
**/
cancelFilter: function() {
cancelFilter() {
this.set('summary', false);
this.set('show_deleted', false);
this.get('userFilters').clear();
},
/**
Toggle summary mode for the stream.
@method toggleSummary
**/
toggleSummary: function() {
toggleSummary() {
this.get('userFilters').clear();
this.toggleProperty('summary');
return this.refresh();
},
toggleDeleted: function() {
toggleDeleted() {
this.toggleProperty('show_deleted');
return this.refresh();
},
/**
Filter the stream to a particular user.
@method toggleParticipant
**/
toggleParticipant: function(username) {
var userFilters = this.get('userFilters');
// Filter the stream to a particular user.
toggleParticipant(username) {
const userFilters = this.get('userFilters');
this.set('summary', false);
this.set('show_deleted', true);
if (userFilters.contains(username)) {
@ -157,22 +135,16 @@ Discourse.PostStream = Em.Object.extend({
/**
Loads a new set of posts into the stream. If you provide a `nearPost` option and the post
is already loaded, it will simply scroll there and load nothing.
@method refresh
@param {Object} opts Options for loading the stream
@param {Integer} opts.nearPost The post we want to find other posts near to.
@param {Boolean} opts.track_visit Whether or not to track this as a visit to a topic.
@returns {Promise} a promise that is resolved when the posts have been inserted into the stream.
**/
refresh: function(opts) {
refresh(opts) {
opts = opts || {};
opts.nearPost = parseInt(opts.nearPost, 10);
var topic = this.get('topic'),
const topic = this.get('topic'),
self = this;
// Do we already have the post in our list of posts? Jump there.
var postWeWant = this.get('posts').findProperty('post_number', opts.nearPost);
const postWeWant = this.get('posts').findProperty('post_number', opts.nearPost);
if (postWeWant) { return Ember.RSVP.resolve(); }
// TODO: if we have all the posts in the filter, don't go to the server for them.
@ -192,10 +164,10 @@ Discourse.PostStream = Em.Object.extend({
},
hasLoadedData: Em.computed.and('hasPosts', 'hasStream'),
collapsePosts: function(from, to){
var posts = this.get('posts');
var remove = posts.filter(function(post){
var postNumber = post.get('post_number');
collapsePosts(from, to){
const posts = this.get('posts');
const remove = posts.filter(function(post){
const postNumber = post.get('post_number');
return postNumber >= from && postNumber <= to;
});
@ -203,9 +175,9 @@ Discourse.PostStream = Em.Object.extend({
// make gap
this.set('gaps', this.get('gaps') || {before: {}, after: {}});
var before = this.get('gaps.before');
const before = this.get('gaps.before');
var post = posts.find(function(post){
const post = posts.find(function(post){
return post.get('post_number') > to;
});
@ -218,16 +190,9 @@ Discourse.PostStream = Em.Object.extend({
},
/**
Fill in a gap of posts before a particular post
@method fillGapBefore
@paaram {Discourse.Post} post beside gap
@paaram {Array} gap array of post ids to load
@returns {Promise} a promise that's resolved when the posts have been added.
**/
fillGapBefore: function(post, gap) {
var postId = post.get('id'),
// Fill in a gap of posts before a particular post
fillGapBefore(post, gap) {
const postId = post.get('id'),
stream = this.get('stream'),
idx = stream.indexOf(postId),
currentPosts = this.get('posts'),
@ -237,11 +202,11 @@ Discourse.PostStream = Em.Object.extend({
// Insert the gap at the appropriate place
stream.splice.apply(stream, [idx, 0].concat(gap));
var postIdx = currentPosts.indexOf(post);
let postIdx = currentPosts.indexOf(post);
if (postIdx !== -1) {
return this.findPostsByIds(gap).then(function(posts) {
posts.forEach(function(p) {
var stored = self.storePost(p);
const stored = self.storePost(p);
if (!currentPosts.contains(stored)) {
currentPosts.insertAt(postIdx++, stored);
}
@ -256,16 +221,9 @@ Discourse.PostStream = Em.Object.extend({
return Ember.RSVP.resolve();
},
/**
Fill in a gap of posts after a particular post
@method fillGapAfter
@paaram {Discourse.Post} post beside gap
@paaram {Array} gap array of post ids to load
@returns {Promise} a promise that's resolved when the posts have been added.
**/
fillGapAfter: function(post, gap) {
var postId = post.get('id'),
// Fill in a gap of posts after a particular post
fillGapAfter(post, gap) {
const postId = post.get('id'),
stream = this.get('stream'),
idx = stream.indexOf(postId),
self = this;
@ -279,24 +237,19 @@ Discourse.PostStream = Em.Object.extend({
return Ember.RSVP.resolve();
},
/**
Appends the next window of posts to the stream. Call it when scrolling downwards.
@method appendMore
@returns {Promise} a promise that's resolved when the posts have been added.
**/
appendMore: function() {
var self = this;
// Appends the next window of posts to the stream. Call it when scrolling downwards.
appendMore() {
const self = this;
// Make sure we can append more posts
if (!self.get('canAppendMore')) { return Ember.RSVP.resolve(); }
var postIds = self.get('nextWindow');
const postIds = self.get('nextWindow');
if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); }
self.set('loadingBelow', true);
var stopLoading = function() {
const stopLoading = function() {
self.set('loadingBelow', false);
};
@ -308,19 +261,14 @@ Discourse.PostStream = Em.Object.extend({
}, stopLoading);
},
/**
Prepend the previous window of posts to the stream. Call it when scrolling upwards.
@method prependMore
@returns {Promise} a promise that's resolved when the posts have been added.
**/
prependMore: function() {
var postStream = this;
// Prepend the previous window of posts to the stream. Call it when scrolling upwards.
prependMore() {
const postStream = this;
// Make sure we can append more posts
if (!postStream.get('canPrependMore')) { return Ember.RSVP.resolve(); }
var postIds = postStream.get('previousWindow');
const postIds = postStream.get('previousWindow');
if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); }
postStream.set('loadingAbove', true);
@ -336,18 +284,13 @@ Discourse.PostStream = Em.Object.extend({
Stage a post for insertion in the stream. It should be rendered right away under the
assumption that the post will succeed. We can then `commitPost` when it succeeds or
`undoPost` when it fails.
@method stagePost
@param {Discourse.Post} post the post to stage in the stream
@param {Discourse.User} user the user creating the post
**/
stagePost: function(post, user) {
stagePost(post, user) {
// We can't stage two posts simultaneously
if (this.get('stagingPost')) { return false; }
this.set('stagingPost', true);
var topic = this.get('topic');
const topic = this.get('topic');
topic.setProperties({
posts_count: (topic.get('posts_count') || 0) + 1,
last_posted_at: new Date(),
@ -371,13 +314,8 @@ Discourse.PostStream = Em.Object.extend({
return true;
},
/**
Commit the post we staged. Call this after a save succeeds.
@method commitPost
@param {Discourse.Post} the post we saved in the stream.
**/
commitPost: function(post) {
// Commit the post we staged. Call this after a save succeeds.
commitPost(post) {
if (this.get('loadedAllPosts')) {
this.appendPost(post);
}
@ -398,16 +336,13 @@ Discourse.PostStream = Em.Object.extend({
/**
Undo a post we've staged in the stream. Remove it from being rendered and revert the
state we changed.
@method undoPost
@param {Discourse.Post} the post to undo from the stream
**/
undoPost: function(post) {
undoPost(post) {
this.get('stream').removeObject(-1);
this.posts.removeObject(post);
this.get('postIdentityMap').set(-1, null);
var topic = this.get('topic');
const topic = this.get('topic');
this.set('stagingPost', false);
topic.setProperties({
@ -418,44 +353,24 @@ Discourse.PostStream = Em.Object.extend({
// TODO unfudge reply count on parent post
},
/**
Prepends a single post to the stream.
@method prependPost
@param {Discourse.Post} post The post we're prepending
@returns {Discourse.Post} the post that was inserted.
**/
prependPost: function(post) {
prependPost(post) {
this.get('posts').unshiftObject(this.storePost(post));
return post;
},
/**
Appends a single post into the stream.
@method appendPost
@param {Discourse.Post} post The post we're appending
@returns {Discourse.Post} the post that was inserted.
**/
appendPost: function(post) {
var stored = this.storePost(post);
appendPost(post) {
const stored = this.storePost(post);
if (stored) {
this.get('posts').addObject(stored);
}
return post;
},
/**
Removes posts from the stream.
@method removePosts
@param {Array} posts the collection of posts to remove
**/
removePosts: function(posts) {
removePosts(posts) {
if (Em.isEmpty(posts)) { return; }
var postIds = posts.map(function (p) { return p.get('id'); });
var identityMap = this.get('postIdentityMap');
const postIds = posts.map(function (p) { return p.get('id'); });
const identityMap = this.get('postIdentityMap');
this.get('stream').removeObjects(postIds);
this.get('posts').removeObjects(posts);
@ -464,14 +379,8 @@ Discourse.PostStream = Em.Object.extend({
});
},
/**
Returns a post from the identity map if it's been inserted.
@method findLoadedPost
@param {Integer} id The post we want from the identity map.
@returns {Discourse.Post} the post that was inserted.
**/
findLoadedPost: function(id) {
// Returns a post from the identity map if it's been inserted.
findLoadedPost(id) {
return this.get('postIdentityMap').get(id);
},
@ -479,17 +388,14 @@ Discourse.PostStream = Em.Object.extend({
Finds and adds a post to the stream by id. Typically this would happen if we receive a message
from the message bus indicating there's a new post. We'll only insert it if we currently
have no filters.
@method triggerNewPostInStream
@param {Integer} postId The id of the new post to be inserted into the stream
**/
triggerNewPostInStream: function(postId) {
triggerNewPostInStream(postId) {
if (!postId) { return; }
// We only trigger if there are no filters active
if (!this.get('hasNoFilters')) { return; }
var loadedAllPosts = this.get('loadedAllPosts');
const loadedAllPosts = this.get('loadedAllPosts');
if (this.get('stream').indexOf(postId) === -1) {
this.get('stream').addObject(postId);
@ -497,8 +403,8 @@ Discourse.PostStream = Em.Object.extend({
}
},
triggerRecoveredPost: function(postId){
var self = this,
triggerRecoveredPost(postId){
const self = this,
postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(postId);
@ -506,15 +412,15 @@ Discourse.PostStream = Em.Object.extend({
this.triggerChangedPost(postId, new Date());
} else {
// need to insert into stream
var url = "/posts/" + postId;
const url = "/posts/" + postId;
Discourse.ajax(url).then(function(p){
var post = Discourse.Post.create(p);
var stream = self.get("stream");
var posts = self.get("posts");
const post = Discourse.Post.create(p);
const stream = self.get("stream");
const posts = self.get("posts");
self.storePost(post);
// we need to zip this into the stream
var index = 0;
let index = 0;
stream.forEach(function(postId){
if(postId < p.id){
index+= 1;
@ -541,13 +447,13 @@ Discourse.PostStream = Em.Object.extend({
}
},
triggerDeletedPost: function(postId){
var self = this,
triggerDeletedPost(postId){
const self = this,
postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(postId);
if(existing){
var url = "/posts/" + postId;
const url = "/posts/" + postId;
Discourse.ajax(url).then(
function(p){
self.storePost(Discourse.Post.create(p));
@ -558,30 +464,24 @@ Discourse.PostStream = Em.Object.extend({
}
},
triggerChangedPost: function(postId, updatedAt) {
triggerChangedPost(postId, updatedAt) {
if (!postId) { return; }
var postIdentityMap = this.get('postIdentityMap'),
const postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(postId),
self = this;
if (existing && existing.updated_at !== updatedAt) {
var url = "/posts/" + postId;
const url = "/posts/" + postId;
Discourse.ajax(url).then(function(p){
self.storePost(Discourse.Post.create(p));
});
}
},
/**
Returns the "thread" of posts in the history of a post.
@method findReplyHistory
@param {Discourse.Post} post the post whose history we want
@returns {Array} the posts in the history.
**/
findReplyHistory: function(post) {
var postStream = this,
// Returns the "thread" of posts in the history of a post.
findReplyHistory(post) {
const postStream = this,
url = "/posts/" + post.get('id') + "/reply-history.json?max_replies=" + Discourse.SiteSettings.max_reply_history;
return Discourse.ajax(url).then(function(result) {
@ -597,16 +497,11 @@ Discourse.PostStream = Em.Object.extend({
Returns the closest post given a postNumber that may not exist in the stream.
For example, if the user asks for a post that's deleted or otherwise outside the range.
This allows us to set the progress bar with the correct number.
@method closestPostForPostNumber
@param {Number} postNumber the post number we're looking for
@return {Post} the closest post
@see PostStream.closestPostNumberFor
**/
closestPostForPostNumber: function(postNumber) {
closestPostForPostNumber(postNumber) {
if (!this.get('hasPosts')) { return; }
var closest = null;
let closest = null;
this.get('posts').forEach(function (p) {
if (!closest) {
closest = p;
@ -628,17 +523,12 @@ Discourse.PostStream = Em.Object.extend({
@returns {Number} 1-starting index of the post, or 0 if not found
@see PostStream.progressIndexOfPostId
**/
progressIndexOfPost: function(post) {
progressIndexOfPost(post) {
return this.progressIndexOfPostId(post.get('id'));
},
/**
Get the index in the stream of a post id. (Use this for the topic progress bar.)
@param post_id - post id to search for
@returns {Number} 1-starting index of the post, or 0 if not found
**/
progressIndexOfPostId: function(post_id) {
// Get the index in the stream of a post id. (Use this for the topic progress bar.)
progressIndexOfPostId(post_id) {
return this.get('stream').indexOf(post_id) + 1;
},
@ -646,15 +536,11 @@ Discourse.PostStream = Em.Object.extend({
Returns the closest post number given a postNumber that may not exist in the stream.
For example, if the user asks for a post that's deleted or otherwise outside the range.
This allows us to set the progress bar with the correct number.
@method closestPostNumberFor
@param {Number} postNumber the post number we're looking for
@return {Number} a close post number
**/
closestPostNumberFor: function(postNumber) {
closestPostNumberFor(postNumber) {
if (!this.get('hasPosts')) { return; }
var closest = null;
let closest = null;
this.get('posts').forEach(function (p) {
if (closest === postNumber) { return; }
if (!closest) { closest = p.get('post_number'); }
@ -668,41 +554,33 @@ Discourse.PostStream = Em.Object.extend({
},
// Find a postId for a postNumber, respecting gaps
findPostIdForPostNumber: function(postNumber) {
var count = 1,
stream = this.get('stream'),
beforeLookup = this.get('gaps.before'),
streamLength = stream.length;
findPostIdForPostNumber(postNumber) {
const stream = this.get('stream'),
beforeLookup = this.get('gaps.before'),
streamLength = stream.length;
for (var i=0; i<streamLength; i++) {
var pid = stream[i];
let sum = 1;
for (let i=0; i<streamLength; i++) {
const pid = stream[i];
// See if there are posts before this post
if (beforeLookup) {
var before = beforeLookup[pid];
const before = beforeLookup[pid];
if (before) {
for (var j=0; j<before.length; j++) {
if (count === postNumber) { return pid; }
count++;
for (let j=0; j<before.length; j++) {
if (sum === postNumber) { return pid; }
sum++;
}
}
}
if (count === postNumber) { return pid; }
count++;
if (sum === postNumber) { return pid; }
sum++;
}
},
/**
@private
Given a JSON packet, update this stream and the posts that exist in it.
@param {Object} postStreamData The JSON data we want to update from.
@method updateFromJson
**/
updateFromJson: function(postStreamData) {
var postStream = this,
updateFromJson(postStreamData) {
const postStream = this,
posts = this.get('posts');
posts.clear();
@ -720,23 +598,17 @@ Discourse.PostStream = Em.Object.extend({
},
/**
@private
Stores a post in our identity map, and sets up the references it needs to
find associated objects like the topic. It might return a different reference
than you supplied if the post has already been loaded.
@method storePost
@param {Discourse.Post} post The post we're storing in the identity map
@returns {Discourse.Post} the post from the identity map
**/
storePost: function(post) {
storePost(post) {
// Calling `Em.get(undefined` raises an error
if (!post) { return; }
var postId = Em.get(post, 'id');
const postId = Em.get(post, 'id');
if (postId) {
var postIdentityMap = this.get('postIdentityMap'),
const postIdentityMap = this.get('postIdentityMap'),
existing = postIdentityMap.get(post.get('id'));
if (existing) {
@ -752,7 +624,7 @@ Discourse.PostStream = Em.Object.extend({
postIdentityMap.set(post.get('id'), post);
// Update the `highest_post_number` if this post is higher.
var postNumber = post.get('post_number');
const postNumber = post.get('post_number');
if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) {
this.set('topic.highest_post_number', postNumber);
}
@ -761,17 +633,11 @@ Discourse.PostStream = Em.Object.extend({
},
/**
@private
Given a list of postIds, returns a list of the posts we don't have in our
identity map and need to load.
@method listUnloadedIds
@param {Array} postIds The post Ids we want to load from the server
@returns {Array} the array of postIds we don't have loaded.
**/
listUnloadedIds: function(postIds) {
var unloaded = Em.A(),
listUnloadedIds(postIds) {
const unloaded = Em.A(),
postIdentityMap = this.get('postIdentityMap');
postIds.forEach(function(p) {
if (!postIdentityMap.has(p)) { unloaded.pushObject(p); }
@ -779,17 +645,8 @@ Discourse.PostStream = Em.Object.extend({
return unloaded;
},
/**
@private
Returns a list of posts in order requested, by id.
@method findPostsByIds
@param {Array} postIds The post Ids we want to retrieve, in order.
@returns {Promise} a promise that will resolve to the posts in the order requested.
**/
findPostsByIds: function(postIds) {
var unloaded = this.listUnloadedIds(postIds),
findPostsByIds(postIds) {
const unloaded = this.listUnloadedIds(postIds),
postIdentityMap = this.get('postIdentityMap');
// Load our unloaded posts by id
@ -800,27 +657,18 @@ Discourse.PostStream = Em.Object.extend({
});
},
/**
@private
Loads a list of posts from the server and inserts them into our identity map.
@method loadIntoIdentityMap
@param {Array} postIds The post Ids we want to insert into the identity map.
@returns {Promise} a promise that will resolve to the posts in the order requested.
**/
loadIntoIdentityMap: function(postIds) {
loadIntoIdentityMap(postIds) {
// If we don't want any posts, return a promise that resolves right away
if (Em.isEmpty(postIds)) {
return Ember.RSVP.resolve();
}
var url = "/t/" + this.get('topic.id') + "/posts.json",
const url = "/t/" + this.get('topic.id') + "/posts.json",
data = { post_ids: postIds },
postStream = this;
return Discourse.ajax(url, {data: data}).then(function(result) {
var posts = Em.get(result, "post_stream.posts");
const posts = Em.get(result, "post_stream.posts");
if (posts) {
posts.forEach(function (p) {
postStream.storePost(Discourse.Post.create(p));
@ -830,33 +678,19 @@ Discourse.PostStream = Em.Object.extend({
},
/**
@private
Returns the index of a particular post in the stream
@method indexOf
@param {Discourse.Post} post The post we're looking for
**/
indexOf: function(post) {
indexOf(post) {
return this.get('stream').indexOf(post.get('id'));
},
/**
@private
Handles an error loading a topic based on a HTTP status code. Updates
the text to the correct values.
@method errorLoading
@param {Integer} status the HTTP status code
@param {Discourse.Topic} topic The topic instance we were trying to load
**/
errorLoading: function(result) {
var status = result.status;
errorLoading(result) {
const status = result.status;
var topic = this.get('topic');
const topic = this.get('topic');
topic.set('loadingFilter', false);
topic.set('errorLoading', true);
@ -887,10 +721,10 @@ Discourse.PostStream = Em.Object.extend({
});
Discourse.PostStream.reopenClass({
PostStream.reopenClass({
create: function() {
var postStream = this._super.apply(this, arguments);
create() {
const postStream = this._super.apply(this, arguments);
postStream.setProperties({
posts: [],
stream: [],
@ -906,9 +740,9 @@ Discourse.PostStream.reopenClass({
return postStream;
},
loadTopicView: function(topicId, args) {
var opts = _.merge({}, args),
url = Discourse.getURL("/t/") + topicId;
loadTopicView(topicId, args) {
const opts = _.merge({}, args);
let url = Discourse.getURL("/t/") + topicId;
if (opts.nearPost) {
url += "/" + opts.nearPost;
}
@ -921,3 +755,5 @@ Discourse.PostStream.reopenClass({
}
});
export default PostStream;

View File

@ -85,15 +85,13 @@ Discourse.Site.reopenClass(Discourse.Singleton, {
if (result.categories) {
result.categoriesById = {};
result.categories = _.map(result.categories, function(c) {
result.categoriesById[c.id] = Discourse.Category.create(c);
return result.categoriesById[c.id];
return result.categoriesById[c.id] = Discourse.Category.create(c);
});
// Associate the categories with their parents
result.categories.forEach(function (c) {
if (c.get('parent_category_id')) {
c.set('parentCategory',
result.categoriesById[c.get('parent_category_id')]);
c.set('parentCategory', result.categoriesById[c.get('parent_category_id')]);
}
});
}

View File

@ -0,0 +1,6 @@
export default Ember.Object.extend({
findAll(type) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
return adapter.findAll(type);
}
});

View File

@ -1,16 +1,13 @@
/**
A model representing a Topic's details that aren't always present, such as a list of participants.
When showing topics in lists and such this information should not be required.
@class TopicDetails
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.TopicDetails = Discourse.Model.extend({
const TopicDetails = Discourse.Model.extend({
loaded: false,
updateFromJson: function(details) {
updateFromJson(details) {
const topic = this.get('topic');
if (details.allowed_users) {
details.allowed_users = details.allowed_users.map(function (u) {
return Discourse.User.create(u);
@ -24,10 +21,9 @@ Discourse.TopicDetails = Discourse.Model.extend({
}
if (details.participants) {
var topic = this.get('topic');
details.participants = details.participants.map(function (p) {
p.topic = topic;
return Em.Object.create(p);
return Ember.Object.create(p);
});
}
@ -59,7 +55,7 @@ Discourse.TopicDetails = Discourse.Model.extend({
}.property('notification_level', 'notifications_reason_id'),
updateNotifications: function(v) {
updateNotifications(v) {
this.set('notification_level', v);
this.set('notifications_reason_id', null);
return Discourse.ajax("/t/" + (this.get('topic.id')) + "/notifications", {
@ -68,7 +64,7 @@ Discourse.TopicDetails = Discourse.Model.extend({
});
},
removeAllowedUser: function(user) {
removeAllowedUser(user) {
var users = this.get('allowed_users'),
username = user.get('username');
@ -80,3 +76,5 @@ Discourse.TopicDetails = Discourse.Model.extend({
});
}
});
export default TopicDetails;

View File

@ -1,8 +1,11 @@
Discourse.Topic = Discourse.Model.extend({
import TopicDetails from 'discourse/models/topic-details';
import PostStream from 'discourse/models/post-stream';
const Topic = Discourse.Model.extend({
// returns createdAt if there's no bumped date
bumpedAt: function() {
var bumpedAt = this.get('bumped_at');
const bumpedAt = this.get('bumped_at');
if (bumpedAt) {
return new Date(bumpedAt);
} else {
@ -20,11 +23,11 @@ Discourse.Topic = Discourse.Model.extend({
}.property('created_at'),
postStream: function() {
return Discourse.PostStream.create({topic: this});
return PostStream.create({topic: this});
}.property(),
details: function() {
return Discourse.TopicDetails.create({topic: this});
return TopicDetails.create({topic: this});
}.property(),
invisible: Em.computed.not('visible'),
@ -35,12 +38,12 @@ Discourse.Topic = Discourse.Model.extend({
}.property('id'),
category: function() {
var categoryId = this.get('category_id');
const categoryId = this.get('category_id');
if (categoryId) {
return Discourse.Category.list().findProperty('id', categoryId);
}
var categoryName = this.get('categoryName');
const categoryName = this.get('categoryName');
if (categoryName) {
return Discourse.Category.list().findProperty('name', categoryName);
}
@ -48,16 +51,16 @@ Discourse.Topic = Discourse.Model.extend({
}.property('category_id', 'categoryName'),
categoryClass: function() {
return 'category-' + Discourse.Category.slugFor(this.get('category'));
}.property('category'),
return 'category-' + this.get('category.fullSlug');
}.property('category.fullSlug'),
shareUrl: function(){
var user = Discourse.User.current();
const user = Discourse.User.current();
return this.get('url') + (user ? '?u=' + user.get('username_lower') : '');
}.property('url'),
url: function() {
var slug = this.get('slug');
let slug = this.get('slug');
if (slug.trim().length === 0) {
slug = "topic";
}
@ -65,8 +68,8 @@ Discourse.Topic = Discourse.Model.extend({
}.property('id', 'slug'),
// Helper to build a Url with a post number
urlForPostNumber: function(postNumber) {
var url = this.get('url');
urlForPostNumber(postNumber) {
let url = this.get('url');
if (postNumber && (postNumber > 0)) {
url += "/" + postNumber;
}
@ -74,7 +77,7 @@ Discourse.Topic = Discourse.Model.extend({
},
totalUnread: function() {
var count = (this.get('unread') || 0) + (this.get('new_posts') || 0);
const count = (this.get('unread') || 0) + (this.get('new_posts') || 0);
return count > 0 ? count : null;
}.property('new_posts', 'unread'),
@ -83,7 +86,7 @@ Discourse.Topic = Discourse.Model.extend({
}.property('url', 'last_read_post_number'),
lastUnreadUrl: function() {
var postNumber = Math.min(this.get('last_read_post_number') + 1, this.get('highest_post_number'));
const postNumber = Math.min(this.get('last_read_post_number') + 1, this.get('highest_post_number'));
return this.urlForPostNumber(postNumber);
}.property('url', 'last_read_post_number', 'highest_post_number'),
@ -107,12 +110,11 @@ Discourse.Topic = Discourse.Model.extend({
// tells us if we are still asynchronously flushing our "recently read" data.
// So take what the browser has seen into consideration.
displayNewPosts: function() {
var delta, result;
var highestSeen = Discourse.Session.currentProp('highestSeenByTopic')[this.get('id')];
const highestSeen = Discourse.Session.currentProp('highestSeenByTopic')[this.get('id')];
if (highestSeen) {
delta = highestSeen - this.get('last_read_post_number');
let delta = highestSeen - this.get('last_read_post_number');
if (delta > 0) {
result = this.get('new_posts') - delta;
let result = this.get('new_posts') - delta;
if (result < 0) {
result = 0;
}
@ -123,7 +125,7 @@ Discourse.Topic = Discourse.Model.extend({
}.property('new_posts', 'id'),
viewsHeat: function() {
var v = this.get('views');
const v = this.get('views');
if( v >= Discourse.SiteSettings.topic_views_heat_high ) return 'heatmap-high';
if( v >= Discourse.SiteSettings.topic_views_heat_medium ) return 'heatmap-med';
if( v >= Discourse.SiteSettings.topic_views_heat_low ) return 'heatmap-low';
@ -137,17 +139,17 @@ Discourse.Topic = Discourse.Model.extend({
isPrivateMessage: Em.computed.equal('archetype', 'private_message'),
isBanner: Em.computed.equal('archetype', 'banner'),
toggleStatus: function(property) {
toggleStatus(property) {
this.toggleProperty(property);
this.saveStatus(property, this.get(property) ? true : false);
},
setStatus: function(property, value) {
setStatus(property, value) {
this.set(property, value);
this.saveStatus(property, value);
},
saveStatus: function(property, value) {
saveStatus(property, value) {
if (property === 'closed' && value === true) {
this.set('details.auto_close_at', null);
}
@ -160,62 +162,55 @@ Discourse.Topic = Discourse.Model.extend({
});
},
makeBanner: function() {
var self = this;
makeBanner() {
const self = this;
return Discourse.ajax('/t/' + this.get('id') + '/make-banner', { type: 'PUT' })
.then(function () { self.set('archetype', 'banner'); });
},
removeBanner: function() {
var self = this;
removeBanner() {
const self = this;
return Discourse.ajax('/t/' + this.get('id') + '/remove-banner', { type: 'PUT' })
.then(function () { self.set('archetype', 'regular'); });
},
estimatedReadingTime: function() {
var wordCount = this.get('word_count');
const wordCount = this.get('word_count');
if (!wordCount) return;
// Avg for 500 words per minute when you account for skimming
return Math.floor(wordCount / 500.0);
}.property('word_count'),
toggleBookmark: function() {
var self = this, firstPost = this.get("postStream.posts")[0];
toggleBookmark() {
const self = this, firstPost = this.get("postStream.posts")[0];
this.toggleProperty('bookmarked');
if (this.get("postStream.firstPostPresent")) { firstPost.toggleProperty("bookmarked"); }
return Discourse.ajax('/t/' + this.get('id') + '/bookmark', {
type: 'PUT',
data: { bookmarked: self.get('bookmarked') },
error: function(error){
self.toggleProperty('bookmarked');
if (self.get("postStream.firstPostPresent")) { firstPost.toggleProperty('bookmarked'); }
}).catch(function(error) {
self.toggleProperty('bookmarked');
if (self.get("postStream.firstPostPresent")) { firstPost.toggleProperty('bookmarked'); }
var showGenericError = true;
let showGenericError = true;
if (error && error.responseText) {
try {
bootbox.alert($.parseJSON(error.responseText).errors);
showGenericError = false;
} catch(e){}
}
if (error && error.responseText) {
try {
bootbox.alert($.parseJSON(error.responseText).errors);
showGenericError = false;
} catch(e){}
}
if(showGenericError){
bootbox.alert(I18n.t('generic_error'));
}
if(showGenericError){
bootbox.alert(I18n.t('generic_error'));
}
});
},
/**
Invite a user to this topic
@method createInvite
@param {String} emailOrUsername The email or username of the user to be invited
**/
createInvite: function(emailOrUsername, groupNames) {
createInvite(emailOrUsername, groupNames) {
return Discourse.ajax("/t/" + this.get('id') + "/invite", {
type: 'POST',
data: { user: emailOrUsername, group_names: groupNames }
@ -223,7 +218,7 @@ Discourse.Topic = Discourse.Model.extend({
},
// Delete this topic
destroy: function(deleted_by) {
destroy(deleted_by) {
this.setProperties({
deleted_at: new Date(),
deleted_by: deleted_by,
@ -237,7 +232,7 @@ Discourse.Topic = Discourse.Model.extend({
},
// Recover this topic if deleted
recover: function() {
recover() {
this.setProperties({
deleted_at: null,
deleted_by: null,
@ -248,14 +243,14 @@ Discourse.Topic = Discourse.Model.extend({
},
// Update our attributes from a JSON result
updateFromJson: function(json) {
updateFromJson(json) {
this.get('details').updateFromJson(json.details);
var keys = Object.keys(json);
const keys = Object.keys(json);
keys.removeObject('details');
keys.removeObject('post_stream');
var topic = this;
const topic = this;
keys.forEach(function (key) {
topic.set(key, json[key]);
});
@ -266,13 +261,8 @@ Discourse.Topic = Discourse.Model.extend({
return this.get('pinned') && this.get('category.isUncategorizedCategory');
}.property('pinned', 'category.isUncategorizedCategory'),
/**
Clears the pin from a topic for the currently logged in user
@method clearPin
**/
clearPin: function() {
var topic = this;
clearPin() {
const topic = this;
// Clear the pin optimistically from the object
topic.set('pinned', false);
@ -287,7 +277,7 @@ Discourse.Topic = Discourse.Model.extend({
});
},
togglePinnedForUser: function() {
togglePinnedForUser() {
if (this.get('pinned')) {
this.clearPin();
} else {
@ -295,13 +285,8 @@ Discourse.Topic = Discourse.Model.extend({
}
},
/**
Re-pins a topic with a cleared pin
@method rePin
**/
rePin: function() {
var topic = this;
rePin() {
const topic = this;
// Clear the pin optimistically from the object
topic.set('pinned', true);
@ -317,12 +302,12 @@ Discourse.Topic = Discourse.Model.extend({
},
// Is the reply to a post directly below it?
isReplyDirectlyBelow: function(post) {
var posts = this.get('postStream.posts');
var postNumber = post.get('post_number');
isReplyDirectlyBelow(post) {
const posts = this.get('postStream.posts');
const postNumber = post.get('post_number');
if (!posts) return;
var postBelow = posts[posts.indexOf(post) + 1];
const postBelow = posts[posts.indexOf(post) + 1];
// If the post directly below's reply_to_post_number is our post number or we are quoted,
// it's considered directly below.
@ -340,7 +325,7 @@ Discourse.Topic = Discourse.Model.extend({
hasExcerpt: Em.computed.and('pinned', 'excerptNotEmpty'),
excerptTruncated: function() {
var e = this.get('excerpt');
const e = this.get('excerpt');
return( e && e.substr(e.length - 8,8) === '&hellip;' );
}.property('excerpt'),
@ -349,7 +334,7 @@ Discourse.Topic = Discourse.Model.extend({
});
Discourse.Topic.reopenClass({
Topic.reopenClass({
NotificationLevel: {
WATCHING: 3,
TRACKING: 2,
@ -357,13 +342,13 @@ Discourse.Topic.reopenClass({
MUTED: 0
},
createActionSummary: function(result) {
createActionSummary(result) {
if (result.actions_summary) {
var lookup = Em.Object.create();
const lookup = Em.Object.create();
result.actions_summary = result.actions_summary.map(function(a) {
a.post = result;
a.actionType = Discourse.Site.current().postActionTypeById(a.id);
var actionSummary = Discourse.ActionSummary.create(a);
const actionSummary = Discourse.ActionSummary.create(a);
lookup.set(a.actionType.get('name_key'), actionSummary);
return actionSummary;
});
@ -371,7 +356,7 @@ Discourse.Topic.reopenClass({
}
},
update: function(topic, props) {
update(topic, props) {
props = JSON.parse(JSON.stringify(props)) || {};
// We support `category_id` and `categoryId` for compatibility
@ -384,7 +369,7 @@ Discourse.Topic.reopenClass({
// allows us to make a distinction between arrays that were not
// sent and arrays that we specifically want to be empty.
Object.keys(props).forEach(function(k) {
var v = props[k];
const v = props[k];
if (v instanceof Array && v.length === 0) {
props[k + '_empty_array'] = true;
}
@ -400,24 +385,16 @@ Discourse.Topic.reopenClass({
});
},
create: function() {
var result = this._super.apply(this, arguments);
create() {
const result = this._super.apply(this, arguments);
this.createActionSummary(result);
return result;
},
/**
Find similar topics to a given title and body
@method findSimilar
@param {String} title The current title
@param {String} body The current body
@returns A promise that will resolve to the topics
**/
findSimilarTo: function(title, body) {
findSimilarTo(title, body) {
return Discourse.ajax("/topics/similar_to", { data: {title: title, raw: body} }).then(function (results) {
if (Array.isArray(results)) {
return results.map(function(topic) { return Discourse.Topic.create(topic); });
return results.map(function(topic) { return Topic.create(topic); });
} else {
return Ember.A();
}
@ -425,14 +402,13 @@ Discourse.Topic.reopenClass({
},
// Load a topic, but accepts a set of filters
find: function(topicId, opts) {
var url = Discourse.getURL("/t/") + topicId;
find(topicId, opts) {
let url = Discourse.getURL("/t/") + topicId;
if (opts.nearPost) {
url += "/" + opts.nearPost;
}
var data = {};
const data = {};
if (opts.postsAfter) {
data.posts_after = opts.postsAfter;
}
@ -461,8 +437,8 @@ Discourse.Topic.reopenClass({
return Discourse.ajax(url + ".json", {data: data});
},
mergeTopic: function(topicId, destinationTopicId) {
var promise = Discourse.ajax("/t/" + topicId + "/merge-topic", {
mergeTopic(topicId, destinationTopicId) {
const promise = Discourse.ajax("/t/" + topicId + "/merge-topic", {
type: 'POST',
data: {destination_topic_id: destinationTopicId}
}).then(function (result) {
@ -472,8 +448,8 @@ Discourse.Topic.reopenClass({
return promise;
},
movePosts: function(topicId, opts) {
var promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
movePosts(topicId, opts) {
const promise = Discourse.ajax("/t/" + topicId + "/move-posts", {
type: 'POST',
data: opts
}).then(function (result) {
@ -483,8 +459,8 @@ Discourse.Topic.reopenClass({
return promise;
},
changeOwners: function(topicId, opts) {
var promise = Discourse.ajax("/t/" + topicId + "/change-owner", {
changeOwners(topicId, opts) {
const promise = Discourse.ajax("/t/" + topicId + "/change-owner", {
type: 'POST',
data: opts
}).then(function (result) {
@ -494,7 +470,7 @@ Discourse.Topic.reopenClass({
return promise;
},
bulkOperation: function(topics, operation) {
bulkOperation(topics, operation) {
return Discourse.ajax("/topics/bulk", {
type: 'PUT',
data: {
@ -504,8 +480,8 @@ Discourse.Topic.reopenClass({
});
},
bulkOperationByFilter: function(filter, operation, categoryId) {
var data = { filter: filter, operation: operation };
bulkOperationByFilter(filter, operation, categoryId) {
const data = { filter: filter, operation: operation };
if (categoryId) data['category_id'] = categoryId;
return Discourse.ajax("/topics/bulk", {
type: 'PUT',
@ -513,12 +489,13 @@ Discourse.Topic.reopenClass({
});
},
resetNew: function() {
resetNew() {
return Discourse.ajax("/topics/reset-new", {type: 'PUT'});
},
idForSlug: function(slug) {
idForSlug(slug) {
return Discourse.ajax("/t/id_for/" + slug);
}
});
export default Topic;

View File

@ -19,7 +19,6 @@ Discourse.UserActionStat = Discourse.Model.extend({
isResponse: function() {
var actionType = this.get('action_type');
return actionType === Discourse.UserAction.TYPES.replies ||
actionType === Discourse.UserAction.TYPES.mentions ||
actionType === Discourse.UserAction.TYPES.quotes;
}.property('action_type')

View File

@ -20,7 +20,6 @@ Discourse.UserStream = Discourse.Model.extend({
var filter = this.get('filter');
if (filter === Discourse.UserAction.TYPES.replies) {
return [Discourse.UserAction.TYPES.replies,
Discourse.UserAction.TYPES.mentions,
Discourse.UserAction.TYPES.quotes].join(",");
}

View File

@ -1,43 +1,49 @@
var ApplicationRoute = Discourse.Route.extend({
const ApplicationRoute = Discourse.Route.extend({
siteTitle: Discourse.computed.setting('title'),
actions: {
_collectTitleTokens: function(tokens) {
_collectTitleTokens(tokens) {
tokens.push(this.get('siteTitle'));
Discourse.set('_docTitle', tokens.join(' - '));
},
showTopicEntrance: function(data) {
// This is here as a bugfix for when an Ember Cloaked view triggers
// a scroll after a controller has been torn down. The real fix
// should be to fix ember cloaking to not do that, but this catches
// it safely just in case.
postChangedRoute: Ember.K,
showTopicEntrance(data) {
this.controllerFor('topic-entrance').send('show', data);
},
composePrivateMessage: function(user) {
var self = this;
composePrivateMessage(user) {
const self = this;
this.transitionTo('userActivity', user).then(function () {
self.controllerFor('user-activity').send('composePrivateMessage', user);
});
},
error: function(err, transition) {
error(err, transition) {
if (err.status === 404) {
// 404
this.intermediateTransitionTo('unknown');
return;
}
var exceptionController = this.controllerFor('exception'),
errorString = err.toString(),
stack = err.stack;
const exceptionController = this.controllerFor('exception'),
stack = err.stack;
// If we have a stack call `toString` on it. It gives us a better
// stack trace since `console.error` uses the stack track of this
// error callback rather than the original error.
let errorString = err.toString();
if (stack) { errorString = stack.toString(); }
if (err.statusText) { errorString = err.statusText; }
var c = window.console;
const c = window.console;
if (c && c.error) {
c.error(errorString);
}
@ -46,7 +52,7 @@ var ApplicationRoute = Discourse.Route.extend({
this.intermediateTransitionTo('exception');
},
showLogin: function() {
showLogin() {
if (this.site.get("isReadOnly")) {
bootbox.alert(I18n.t("read_only_mode.login_disabled"));
} else {
@ -54,7 +60,7 @@ var ApplicationRoute = Discourse.Route.extend({
}
},
showCreateAccount: function() {
showCreateAccount() {
if (this.site.get("isReadOnly")) {
bootbox.alert(I18n.t("read_only_mode.login_disabled"));
} else {
@ -62,8 +68,8 @@ var ApplicationRoute = Discourse.Route.extend({
}
},
autoLogin: function(modal, onFail){
var methods = Em.get('Discourse.LoginMethod.all');
autoLogin(modal, onFail){
const methods = Em.get('Discourse.LoginMethod.all');
if (!Discourse.SiteSettings.enable_local_logins &&
methods.length === 1) {
Discourse.Route.showModal(this, modal);
@ -73,26 +79,26 @@ var ApplicationRoute = Discourse.Route.extend({
}
},
showForgotPassword: function() {
showForgotPassword() {
Discourse.Route.showModal(this, 'forgotPassword');
},
showNotActivated: function(props) {
showNotActivated(props) {
Discourse.Route.showModal(this, 'notActivated');
this.controllerFor('notActivated').setProperties(props);
},
showUploadSelector: function(composerView) {
showUploadSelector(composerView) {
Discourse.Route.showModal(this, 'uploadSelector');
this.controllerFor('upload-selector').setProperties({ composerView: composerView });
},
showKeyboardShortcutsHelp: function() {
showKeyboardShortcutsHelp() {
Discourse.Route.showModal(this, 'keyboardShortcutsHelp');
},
showSearchHelp: function() {
var self = this;
showSearchHelp() {
const self = this;
// TODO: @EvitTrout how do we get a loading indicator here?
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(function(html){
@ -107,7 +113,7 @@ var ApplicationRoute = Discourse.Route.extend({
@method closeModal
**/
closeModal: function() {
closeModal() {
this.render('hide-modal', {into: 'modal', outlet: 'modalBody'});
},
@ -118,7 +124,7 @@ var ApplicationRoute = Discourse.Route.extend({
@method hideModal
**/
hideModal: function() {
hideModal() {
$('#discourse-modal').modal('hide');
},
@ -127,13 +133,12 @@ var ApplicationRoute = Discourse.Route.extend({
@method showModal
**/
showModal: function() {
showModal() {
$('#discourse-modal').modal('show');
},
editCategory: function(category) {
var self = this;
editCategory(category) {
const self = this;
Discourse.Category.reloadById(category.get('id')).then(function (c) {
self.site.updateCategory(c);
Discourse.Route.showModal(self, 'editCategory', c);
@ -156,7 +161,7 @@ var ApplicationRoute = Discourse.Route.extend({
}
},
activate: function() {
activate() {
this._super();
Em.run.next(function() {
// Support for callbacks once the application has activated
@ -164,11 +169,11 @@ var ApplicationRoute = Discourse.Route.extend({
});
},
handleShowLogin: function() {
var self = this;
handleShowLogin() {
const self = this;
if(Discourse.SiteSettings.enable_sso) {
var returnPath = encodeURIComponent(window.location.pathname);
const returnPath = encodeURIComponent(window.location.pathname);
window.location = Discourse.getURL('/session/sso?return_path=' + returnPath);
} else {
this.send('autoLogin', 'login', function(){
@ -178,8 +183,8 @@ var ApplicationRoute = Discourse.Route.extend({
}
},
handleShowCreateAccount: function() {
var self = this;
handleShowCreateAccount() {
const self = this;
self.send('autoLogin', 'createAccount', function(){
Discourse.Route.showModal(self, 'createAccount');

View File

@ -1,7 +1,7 @@
import { queryParams } from 'discourse/controllers/discovery-sortable';
// A helper to build a topic route for a filter
export function filterQueryParams(params, defaultParams) {
function filterQueryParams(params, defaultParams) {
var findOpts = defaultParams || {};
if (params) {
Ember.keys(queryParams).forEach(function(opt) {
@ -74,3 +74,4 @@ export default function(filter, extras) {
}, extras);
}
export { filterQueryParams };

View File

@ -26,6 +26,14 @@ Discourse.Route = Ember.Route.extend({
},
actions: {
// Ember doesn't provider a router `willTransition` event so let's make one
willTransition: function() {
var router = this.container.lookup('router:main');
Ember.run.once(router, router.trigger, 'willTransition');
return this._super();
},
_collectTitleTokens: function(tokens) {
// If there's a title token method, call it and get the token
if (this.titleToken) {
@ -89,7 +97,8 @@ Discourse.Route.reopenClass({
},
mapRoutes: function() {
var resources = {};
var resources = {},
paths = {};
// If a module is defined as `route-map` in discourse or a plugin, its routes
// will be built automatically. You can supply a `resource` property to
@ -107,6 +116,7 @@ Discourse.Route.reopenClass({
if (!resources[mapObj.resource]) { resources[mapObj.resource] = []; }
resources[mapObj.resource].push(mapObj.map);
if (mapObj.path) { paths[mapObj.resource] = mapObj.path; }
}
});
@ -121,13 +131,37 @@ Discourse.Route.reopenClass({
delete resources.root;
}
// Apply other resources next
// Even if no plugins set it up, we need an `adminPlugins` route
var adminPlugins = 'admin.adminPlugins';
resources[adminPlugins] = resources[adminPlugins] || [Ember.K];
paths[adminPlugins] = paths[adminPlugins] || "/plugins";
var segments = {},
standalone = [];
Object.keys(resources).forEach(function(r) {
router.resource(r, function() {
var m = /^([^\.]+)\.(.*)$/.exec(r);
if (m) {
segments[m[1]] = m[2];
} else {
standalone.push(r);
}
});
// Apply other resources next. A little hacky but works!
standalone.forEach(function(r) {
router.resource(r, {path: paths[r]}, function() {
var res = this;
resources[r].forEach(function(m) {
m.call(res);
});
resources[r].forEach(function(m) { m.call(res); });
var s = segments[r];
if (s) {
var full = r + '.' + s;
res.resource(s, {path: paths[full]}, function() {
var nestedRes = this;
resources[full].forEach(function(m) { m.call(nestedRes); });
});
}
});
});

View File

@ -1,6 +1,8 @@
import Topic from 'discourse/models/topic';
export default Discourse.Route.extend({
model: function(params) {
return Discourse.Topic.idForSlug(params.slug);
return Topic.idForSlug(params.slug);
},
afterModel: function(result) {

View File

@ -4,6 +4,7 @@ var isTransitioning = false,
SCROLL_DELAY = 500;
import ShowFooter from "discourse/mixins/show-footer";
import Topic from 'discourse/models/topic';
var TopicRoute = Discourse.Route.extend(ShowFooter, {
redirect: function() { return this.redirectIfLoginRequired(); },
@ -20,7 +21,8 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, {
var result = model.get('title'),
cat = model.get('category');
if (cat && !cat.get('isUncategorized')) {
// Only display uncategorized in the title tag if it was renamed
if (cat && !(cat.get('isUncategorizedCategory') && cat.get('name').toLowerCase() === "uncategorized")) {
var catName = cat.get('name'),
parentCategory = cat.get('parentCategory');
@ -95,7 +97,6 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, {
// Use replaceState to update the URL once it changes
postChangedRoute: function(currentPost) {
// do nothing if we are transitioning to another route
if (isTransitioning || Discourse.TopicRoute.disableReplaceState) { return; }
@ -163,7 +164,7 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, {
return topic;
});
} else {
return this.setupParams(Discourse.Topic.create(_.omit(params, 'username_filters', 'filter')), queryParams);
return this.setupParams(Topic.create(_.omit(params, 'username_filters', 'filter')), queryParams);
}
},

View File

@ -0,0 +1,5 @@
import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
export default UserActivityStreamRoute.extend({
userActionType: Discourse.UserAction.TYPES["mentions"]
});

View File

@ -1,6 +1,6 @@
{{render "header"}}
<div id='main-outlet' {{bind-attr class=backgroundClass}}>
<div id='main-outlet'>
{{outlet}}
{{render "user-card"}}
</div>

Some files were not shown because too many files have changed in this diff Show More