Version bump
This commit is contained in:
commit
3739684164
5
Gemfile
5
Gemfile
@ -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
|
||||
|
||||
|
||||
15
Gemfile.lock
15
Gemfile.lock
@ -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
|
||||
|
||||
@ -17,6 +17,7 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
|
||||
[](http://discuss.atom.io)
|
||||
[](http://discourse.soylent.me)
|
||||
|
||||
Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/).
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@ -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')
|
||||
});
|
||||
@ -53,6 +53,9 @@ export default Ember.Controller.extend({
|
||||
actions: {
|
||||
refreshProblems: function() {
|
||||
this.loadProblems();
|
||||
},
|
||||
showTrafficReport: function() {
|
||||
this.set("showTrafficReport", true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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"),
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export default Ember.ArrayController.extend({
|
||||
|
||||
adminRoutes: function() {
|
||||
return this.get('model').map(p => p.admin_route).compact();
|
||||
}.property()
|
||||
});
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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]);
|
||||
});
|
||||
|
||||
|
||||
@ -2,4 +2,4 @@ export default Discourse.Route.extend({
|
||||
redirect: function() {
|
||||
this.transitionTo("adminGroupsType", "custom");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@ -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);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
12
app/assets/javascripts/admin/routes/admin-plugins.js.es6
Normal file
12
app/assets/javascripts/admin/routes/admin-plugins.js.es6
Normal file
@ -0,0 +1,12 @@
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.store.findAll('plugin');
|
||||
},
|
||||
|
||||
actions: {
|
||||
showSettings() {
|
||||
this.transitionTo('adminSiteSettingsCategory', 'plugins');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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'} );
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
@ -0,0 +1,7 @@
|
||||
<div class='admin-controls'>
|
||||
<div class='span15'>
|
||||
<ul class="nav nav-pills">
|
||||
{{yield}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -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}}
|
||||
|
||||
@ -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> </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">
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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}}
|
||||
|
||||
39
app/assets/javascripts/admin/templates/plugins-index.hbs
Normal file
39
app/assets/javascripts/admin/templates/plugins-index.hbs
Normal 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>
|
||||
|
||||
13
app/assets/javascripts/admin/templates/plugins.hbs
Normal file
13
app/assets/javascripts/admin/templates/plugins.hbs
Normal 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>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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() {
|
||||
|
||||
46
app/assets/javascripts/discourse/adapters/rest.js.es6
Normal file
46
app/assets/javascripts/discourse/adapters/rest.js.es6
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
@ -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 {
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
});
|
||||
|
||||
@ -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.
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -26,6 +26,6 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
sources: function() {
|
||||
return Sharing.activeSources();
|
||||
return Sharing.activeSources(this.siteSettings.share_links);
|
||||
}.property()
|
||||
});
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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; }
|
||||
|
||||
|
||||
@ -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")
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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/')];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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>');
|
||||
}
|
||||
});
|
||||
|
||||
@ -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});
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@ -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'});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
10
app/assets/javascripts/discourse/lib/highlight-syntax.js.es6
Normal file
10
app/assets/javascripts/discourse/lib/highlight-syntax.js.es6
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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', '×'),
|
||||
'<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>'
|
||||
].join(' · ');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
45
app/assets/javascripts/discourse/lib/lightbox.js.es6
Normal file
45
app/assets/javascripts/discourse/lib/lightbox.js.es6
Normal 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', '×'),
|
||||
'<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>'
|
||||
].join(' · ');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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() &&
|
||||
|
||||
@ -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')
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -13,4 +13,4 @@ export default Em.Mixin.create({
|
||||
return true;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@ -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') };
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
@ -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')]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
6
app/assets/javascripts/discourse/models/store.js.es6
Normal file
6
app/assets/javascripts/discourse/models/store.js.es6
Normal 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);
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
@ -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) === '…' );
|
||||
}.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;
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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(",");
|
||||
}
|
||||
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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); });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
|
||||
|
||||
export default UserActivityStreamRoute.extend({
|
||||
userActionType: Discourse.UserAction.TYPES["mentions"]
|
||||
});
|
||||
@ -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
Reference in New Issue
Block a user