Version bump

This commit is contained in:
Neil Lalonde 2015-03-24 14:18:38 -04:00
commit c7040e46b9
437 changed files with 6362 additions and 2006 deletions

View File

@ -12,6 +12,7 @@ lib/javascripts/locale/
lib/javascripts/messageformat.js
lib/javascripts/moment.js
lib/javascripts/moment_locale/
lib/highlight_js/
lib/es6_module_transpiler/support/es6-module-transpiler.js
public/javascripts/
spec/phantom_js/smoke_test.js

View File

@ -213,7 +213,7 @@ GEM
mime-types (~> 1.16)
treetop (~> 1.4.8)
memory_profiler (0.9.0)
message_bus (1.0.6)
message_bus (1.0.9)
eventmachine
rack (>= 1.1.3)
redis

View File

@ -1,31 +1,36 @@
export default Ember.Component.extend({
tagName: 'div',
didInsertElement: function(){
_init: function(){
this.$("input").select2({
multiple: true,
width: '100%',
query: function(opts){
opts.callback({
results: this.get("available").map(this._format)
});
query: function(opts) {
opts.callback({ results: this.get("available").map(this._format) });
}.bind(this)
}).on("change", function(evt) {
if (evt.added){
this.triggerAction({action: "groupAdded",
actionContext: this.get("available"
).findBy("id", evt.added.id)});
this.triggerAction({
action: "groupAdded",
actionContext: this.get("available").findBy("id", evt.added.id)
});
} else if (evt.removed) {
this.triggerAction({action:"groupRemoved",
actionContext: this.get("selected"
).findBy("id", evt.removed.id)});
this.triggerAction({
action:"groupRemoved",
actionContext: evt.removed.id
});
}
}.bind(this));
this._refreshOnReset();
},
_format: function(item){
return {"text": item.name, "id": item.id, "locked": item.automatic};
this._refreshOnReset();
}.on("didInsertElement"),
_format(item) {
return {
"text": item.name,
"id": item.id,
"locked": item.automatic
};
},
_refreshOnReset: function() {

View File

@ -11,58 +11,61 @@ export default ObjectController.extend(CanCheckEmails, {
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
custom_groups: Ember.computed.filter("model.groups", function(g){
return (!g.automatic && g.visible);
}),
automaticGroups: function() {
return this.get("model.automaticGroups").map((g) => g.name).join(", ");
}.property("model.automaticGroups"),
userFields: function() {
var siteUserFields = this.site.get('user_fields'),
userFields = this.get('user_fields');
const siteUserFields = this.site.get('user_fields'),
userFields = this.get('user_fields');
if (!Ember.isEmpty(siteUserFields)) {
return siteUserFields.map(function(uf) {
var value = userFields ? userFields[uf.get('id').toString()] : null;
return {name: uf.get('name'), value: value};
let value = userFields ? userFields[uf.get('id').toString()] : null;
return { name: uf.get('name'), value: value };
});
}
return [];
}.property('user_fields.@each'),
actions: {
toggleTitleEdit: function() {
toggleTitleEdit() {
this.toggleProperty('editingTitle');
},
saveTitle: function() {
Discourse.ajax("/users/" + this.get('username').toLowerCase(), {
saveTitle() {
const self = this;
return Discourse.ajax("/users/" + this.get('username').toLowerCase(), {
data: {title: this.get('title')},
type: 'PUT'
}).then(null, function(e){
}).catch(function(e) {
bootbox.alert(I18n.t("generic_error_with_reason", {error: "http: " + e.status + " - " + e.body}));
}).finally(function() {
self.send('toggleTitleEdit');
});
this.send('toggleTitleEdit');
},
generateApiKey: function() {
generateApiKey() {
this.get('model').generateApiKey();
},
groupAdded: function(added){
groupAdded(added) {
this.get('model').groupAdded(added).catch(function() {
bootbox.alert(I18n.t('generic_error'));
});
},
groupRemoved: function(removed){
this.get('model').groupRemoved(removed).catch(function() {
groupRemoved(groupId) {
this.get('model').groupRemoved(groupId).catch(function() {
bootbox.alert(I18n.t('generic_error'));
});
},
savePrimaryGroup: function() {
var self = this;
Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", {
savePrimaryGroup() {
const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", {
type: 'PUT',
data: {primary_group_id: this.get('primary_group_id')}
}).then(function () {
@ -72,33 +75,41 @@ export default ObjectController.extend(CanCheckEmails, {
});
},
resetPrimaryGroup: function() {
resetPrimaryGroup() {
this.set('primary_group_id', this.get('originalPrimaryGroupId'));
},
regenerateApiKey: function() {
var self = this;
bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
if (result) {
self.get('model').generateApiKey();
regenerateApiKey() {
const self = this;
bootbox.confirm(
I18n.t("admin.api.confirm_regen"),
I18n.t("no_value"),
I18n.t("yes_value"),
function(result) {
if (result) { self.get('model').generateApiKey(); }
}
});
);
},
revokeApiKey: function() {
var self = this;
bootbox.confirm(I18n.t("admin.api.confirm_revoke"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
if (result) {
self.get('model').revokeApiKey();
revokeApiKey() {
const self = this;
bootbox.confirm(
I18n.t("admin.api.confirm_revoke"),
I18n.t("no_value"),
I18n.t("yes_value"),
function(result) {
if (result) { self.get('model').revokeApiKey(); }
}
});
);
},
anonymize: function() {
anonymize() {
this.get('model').anonymize();
},
destroy: function() {
destroy() {
this.get('model').destroy();
}
}

View File

@ -2,7 +2,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
import Controller from 'discourse/controllers/controller';
export default Controller.extend(ModalFunctionality, {
needs: ["adminBackupsLogs"],
_startBackup: function (withUploads) {
@ -17,11 +16,11 @@ export default Controller.extend(ModalFunctionality, {
actions: {
startBackup: function () {
return this._startBackup();
this._startBackup();
},
startBackupWithoutUpload: function () {
return this._startBackup(false);
this._startBackup(false);
},
cancel: function () {

View File

@ -1,5 +1,4 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
export default ObjectController.extend(ModalFunctionality, {

View File

@ -1,7 +1,7 @@
import ChangeSiteCustomizationDetailsController from "admin/controllers/change-site-customization-details";
import ChangeSiteCustomizationDetailsController from "admin/controllers/modals/change-site-customization-details";
export default ChangeSiteCustomizationDetailsController.extend({
onShow: function() {
this.selectPrevious();
this.send("selectPrevious");
}
});

View File

@ -1,58 +1,36 @@
/**
Our data model for dealing with users from the admin section.
const AdminUser = Discourse.User.extend({
@class AdminUser
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.AdminUser = Discourse.User.extend({
customGroups: Em.computed.filter("groups", (g) => !g.automatic && g.visible && Discourse.Group.create(g)),
automaticGroups: Em.computed.filter("groups", (g) => g.automatic && Discourse.Group.create(g)),
/**
Generates an API key for the user. Will regenerate if they already have one.
@method generateApiKey
@returns {Promise} a promise that resolves to the newly generated API key
**/
generateApiKey: function() {
var self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", {type: 'POST'}).then(function (result) {
var apiKey = Discourse.ApiKey.create(result.api_key);
generateApiKey() {
const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", {
type: 'POST'
}).then(function (result) {
const apiKey = Discourse.ApiKey.create(result.api_key);
self.set('api_key', apiKey);
return apiKey;
});
},
groupAdded: function(added){
var self = this;
groupAdded(added) {
return Discourse.ajax("/admin/users/" + this.get('id') + "/groups", {
type: 'POST',
data: {group_id: added.id}
}).then(function () {
self.get('groups').pushObject(added);
});
data: { group_id: added.id }
}).then(() => this.get('groups').pushObject(added));
},
groupRemoved: function(removed){
var self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/groups/" + removed.id, {
groupRemoved(groupId) {
return Discourse.ajax("/admin/users/" + this.get('id') + "/groups/" + groupId, {
type: 'DELETE'
}).then(function () {
self.set('groups.[]', self.get('groups').rejectBy("id", removed.id));
});
}).then(() => this.set('groups.[]', this.get('groups').rejectBy("id", groupId)));
},
/**
Revokes a user's current API key
@method revokeApiKey
@returns {Promise} a promise that resolves when the API key has been deleted
**/
revokeApiKey: function() {
var self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/revoke_api_key", {type: 'DELETE'}).then(function () {
self.set('api_key', null);
});
revokeApiKey() {
return Discourse.ajax("/admin/users/" + this.get('id') + "/revoke_api_key", {
type: 'DELETE'
}).then(() => this.set('api_key', null));
},
deleteAllPostsExplanation: function() {
@ -70,99 +48,111 @@ Discourse.AdminUser = Discourse.User.extend({
}
}.property('can_delete_all_posts', 'deleteForbidden'),
deleteAllPosts: function() {
var user = this;
var message = I18n.t('admin.user.delete_all_posts_confirm', {posts: user.get('post_count'), topics: user.get('topic_count')});
var buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel-inline",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i> ' + I18n.t("admin.user.delete_all_posts"),
"class": "btn btn-danger",
"callback": function() {
Discourse.ajax("/admin/users/" + (user.get('id')) + "/delete_all_posts", {type: 'PUT'}).then(function(){
user.set('post_count', 0);
});
}
}];
bootbox.dialog(message, buttons, {"classes": "delete-all-posts"});
deleteAllPosts() {
const user = this,
message = I18n.t('admin.user.delete_all_posts_confirm', { posts: user.get('post_count'), topics: user.get('topic_count') }),
buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel-inline",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i> ' + I18n.t("admin.user.delete_all_posts"),
"class": "btn btn-danger",
"callback": function() {
Discourse.ajax("/admin/users/" + user.get('id') + "/delete_all_posts", {
type: 'PUT'
}).then(() => user.set('post_count', 0));
}
}];
bootbox.dialog(message, buttons, { "classes": "delete-all-posts" });
},
// Revoke the user's admin access
revokeAdmin: function() {
this.set('admin', false);
this.set('can_grant_admin', true);
this.set('can_revoke_admin', false);
return Discourse.ajax("/admin/users/" + (this.get('id')) + "/revoke_admin", {type: 'PUT'});
},
grantAdmin: function() {
this.set('admin', true);
this.set('can_grant_admin', false);
this.set('can_revoke_admin', true);
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);
revokeAdmin() {
const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/revoke_admin", {
type: 'PUT'
}).then(function() {
self.setProperties({
admin: false,
can_grant_admin: true,
can_revoke_admin: false
});
});
},
// Revoke the user's moderation access
revokeModeration: function() {
this.set('moderator', false);
this.set('can_grant_moderation', true);
this.set('can_revoke_moderation', false);
return Discourse.ajax("/admin/users/" + (this.get('id')) + "/revoke_moderation", {type: 'PUT'});
grantAdmin() {
const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/grant_admin", {
type: 'PUT'
}).then(function() {
self.setProperties({
admin: true,
can_grant_admin: false,
can_revoke_admin: true
});
}).catch(function(e) {
let error;
if (e.responseJSON && e.responseJSON.error) {
error = e.responseJSON.error;
}
error = error || I18n.t('admin.user.grant_admin_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
},
grantModeration: function() {
this.set('moderator', true);
this.set('can_grant_moderation', false);
this.set('can_revoke_moderation', true);
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);
});
revokeModeration() {
const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/revoke_moderation", {
type: 'PUT'
}).then(function() {
self.setProperties({
moderator: false,
can_grant_moderation: true,
can_revoke_moderation: false
});
});
},
refreshBrowsers: function() {
Discourse.ajax("/admin/users/" + (this.get('id')) + "/refresh_browsers", {type: 'POST'});
bootbox.alert(I18n.t("admin.user.refresh_browsers_message"));
grantModeration() {
const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/grant_moderation", {
type: 'PUT'
}).then(function() {
self.setProperties({
moderator: true,
can_grant_moderation: false,
can_revoke_moderation: true
});
}).catch(function(e) {
let error;
if (e.responseJSON && e.responseJSON.error) {
error = e.responseJSON.error;
}
error = error || I18n.t('admin.user.grant_moderation_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
},
approve: function() {
this.set('can_approve', false);
this.set('approved', true);
this.set('approved_by', Discourse.User.current());
Discourse.ajax("/admin/users/" + (this.get('id')) + "/approve", {type: 'PUT'});
refreshBrowsers() {
return Discourse.ajax("/admin/users/" + this.get('id') + "/refresh_browsers", {
type: 'POST'
}).finally(() => bootbox.alert(I18n.t("admin.user.refresh_browsers_message")));
},
username_lower: (function() {
return this.get('username').toLowerCase();
}).property('username'),
approve() {
const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/approve", {
type: 'PUT'
}).then(function() {
self.setProperties({
can_approve: false,
approved: true,
approved_by: Discourse.User.current()
});
});
},
setOriginalTrustLevel: function() {
setOriginalTrustLevel() {
this.set('originalTrustLevel', this.get('trust_level'));
},
@ -172,16 +162,14 @@ Discourse.AdminUser = Discourse.User.extend({
dirty: Discourse.computed.propertyNotEqual('originalTrustLevel', 'trustLevel.id'),
saveTrustLevel: function() {
Discourse.ajax("/admin/users/" + this.id + "/trust_level", {
saveTrustLevel() {
return Discourse.ajax("/admin/users/" + this.id + "/trust_level", {
type: 'PUT',
data: {level: this.get('trustLevel.id')}
}).then(function () {
// succeeded
data: { level: this.get('trustLevel.id') }
}).then(function() {
window.location.reload();
}, function(e) {
// failure
var error;
}).catch(function(e) {
let error;
if (e.responseJSON && e.responseJSON.errors) {
error = e.responseJSON.errors[0];
}
@ -190,20 +178,18 @@ Discourse.AdminUser = Discourse.User.extend({
});
},
restoreTrustLevel: function() {
restoreTrustLevel() {
this.set('trustLevel.id', this.get('originalTrustLevel'));
},
lockTrustLevel: function(locked) {
Discourse.ajax("/admin/users/" + this.id + "/trust_level_lock", {
lockTrustLevel(locked) {
return Discourse.ajax("/admin/users/" + this.id + "/trust_level_lock", {
type: 'PUT',
data: { locked: !!locked }
}).then(function() {
// succeeded
window.location.reload();
}, function(e) {
// failure
var error;
}).catch(function(e) {
let error;
if (e.responseJSON && e.responseJSON.errors) {
error = e.responseJSON.errors[0];
}
@ -212,7 +198,7 @@ Discourse.AdminUser = Discourse.User.extend({
});
},
canLockTrustLevel: function(){
canLockTrustLevel: function() {
return this.get('trust_level') < 4;
}.property('trust_level'),
@ -220,51 +206,45 @@ Discourse.AdminUser = Discourse.User.extend({
canSuspend: Em.computed.not('staff'),
suspendDuration: function() {
var suspended_at = moment(this.suspended_at);
var suspended_till = moment(this.suspended_till);
const suspended_at = moment(this.suspended_at),
suspended_till = moment(this.suspended_till);
return suspended_at.format('L') + " - " + suspended_till.format('L');
}.property('suspended_till', 'suspended_at'),
suspend: function(duration, reason) {
suspend(duration, reason) {
return Discourse.ajax("/admin/users/" + this.id + "/suspend", {
type: 'PUT',
data: {duration: duration, reason: reason}
data: { duration: duration, reason: reason }
});
},
unsuspend: function() {
Discourse.ajax("/admin/users/" + this.id + "/unsuspend", {
unsuspend() {
return Discourse.ajax("/admin/users/" + this.id + "/unsuspend", {
type: 'PUT'
}).then(function() {
// succeeded
window.location.reload();
}, function(e) {
// failed
}).catch(function(e) {
var error = I18n.t('admin.user.unsuspend_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
},
log_out: function(){
Discourse.ajax("/admin/users/" + this.id + "/log_out", {
type: 'POST',
data: { username_or_email: this.get('username') }
}).then(
function(){
bootbox.alert(I18n.t("admin.user.logged_out"));
}
);
},
impersonate: function() {
Discourse.ajax("/admin/impersonate", {
log_out() {
return Discourse.ajax("/admin/users/" + this.id + "/log_out", {
type: 'POST',
data: { username_or_email: this.get('username') }
}).then(function() {
bootbox.alert(I18n.t("admin.user.logged_out"));
});
},
impersonate() {
return Discourse.ajax("/admin/impersonate", {
type: 'POST',
data: { username_or_email: this.get('username') }
}).then(function() {
// succeeded
document.location = "/";
}, function(e) {
// failed
}).catch(function(e) {
if (e.status === 404) {
bootbox.alert(I18n.t('admin.impersonate.not_found'));
} else {
@ -273,56 +253,57 @@ Discourse.AdminUser = Discourse.User.extend({
});
},
activate: function() {
Discourse.ajax('/admin/users/' + this.id + '/activate', {type: 'PUT'}).then(function() {
// succeeded
activate() {
return Discourse.ajax('/admin/users/' + this.id + '/activate', {
type: 'PUT'
}).then(function() {
window.location.reload();
}, function(e) {
// failed
}).catch(function(e) {
var error = I18n.t('admin.user.activate_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
},
deactivate: function() {
Discourse.ajax('/admin/users/' + this.id + '/deactivate', {type: 'PUT'}).then(function() {
// succeeded
deactivate() {
return Discourse.ajax('/admin/users/' + this.id + '/deactivate', {
type: 'PUT'
}).then(function() {
window.location.reload();
}, function(e) {
// failed
}).catch(function(e) {
var error = I18n.t('admin.user.deactivate_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
},
unblock: function() {
Discourse.ajax('/admin/users/' + this.id + '/unblock', {type: 'PUT'}).then(function() {
// succeeded
unblock() {
return Discourse.ajax('/admin/users/' + this.id + '/unblock', {
type: 'PUT'
}).then(function() {
window.location.reload();
}, function(e) {
// failed
}).catch(function(e) {
var error = I18n.t('admin.user.unblock_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
},
block: function() {
Discourse.ajax('/admin/users/' + this.id + '/block', {type: 'PUT'}).then(function() {
// succeeded
block() {
return Discourse.ajax('/admin/users/' + this.id + '/block', {
type: 'PUT'
}).then(function() {
window.location.reload();
}, function(e) {
// failed
}).catch(function(e) {
var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
},
sendActivationEmail: function() {
Discourse.ajax('/users/action/send_activation_email', {data: {username: this.get('username')}, type: 'POST'}).then(function() {
// succeeded
sendActivationEmail() {
return Discourse.ajax('/users/action/send_activation_email', {
type: 'POST',
data: { username: this.get('username') }
}).then(function() {
bootbox.alert( I18n.t('admin.user.activation_email_sent') );
}, function(e) {
// failed
}).catch(function(e) {
var error = I18n.t('admin.user.send_activation_email_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
@ -330,11 +311,14 @@ Discourse.AdminUser = Discourse.User.extend({
anonymizeForbidden: Em.computed.not("can_be_anonymized"),
anonymize: function() {
var user = this;
anonymize() {
const user = this,
message = I18n.t("admin.user.anonymize_confirm");
var performAnonymize = function() {
Discourse.ajax("/admin/users/" + user.get('id') + '/anonymize.json', {type: 'PUT'}).then(function(data) {
const performAnonymize = function() {
return Discourse.ajax("/admin/users/" + user.get('id') + '/anonymize.json', {
type: 'PUT'
}).then(function(data) {
if (data.success) {
if (data.username) {
document.location = "/admin/users/" + data.username;
@ -347,26 +331,22 @@ Discourse.AdminUser = Discourse.User.extend({
user.setProperties(data.user);
}
}
}, function() {
}).catch(function() {
bootbox.alert(I18n.t("admin.user.anonymize_failed"));
});
};
var message = I18n.t("admin.user.anonymize_confirm");
var buttons = [{
const buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.anonymize_yes'),
"class": "btn btn-danger",
"callback": function(){
performAnonymize();
}
"callback": function() { performAnonymize(); }
}];
bootbox.dialog(message, buttons, {"classes": "delete-user-modal"});
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
},
deleteForbidden: Em.computed.not("canBeDeleted"),
@ -383,12 +363,13 @@ Discourse.AdminUser = Discourse.User.extend({
}
}.property('deleteForbidden'),
destroy: function(opts) {
var user = this;
var location = document.location.pathname;
destroy(opts) {
const user = this,
message = I18n.t("admin.user.delete_confirm"),
location = document.location.pathname;
var performDestroy = function(block) {
var formData = { context: location };
const performDestroy = function(block) {
let formData = { context: location };
if (block) {
formData["block_email"] = true;
formData["block_urls"] = true;
@ -397,7 +378,7 @@ Discourse.AdminUser = Discourse.User.extend({
if (opts && opts.deletePosts) {
formData["delete_posts"] = true;
}
Discourse.ajax("/admin/users/" + user.get('id') + '.json', {
return Discourse.ajax("/admin/users/" + user.get('id') + '.json', {
type: 'DELETE',
data: formData
}).then(function(data) {
@ -413,47 +394,42 @@ Discourse.AdminUser = Discourse.User.extend({
user.setProperties(data.user);
}
}
}, function() {
}).catch(function() {
Discourse.AdminUser.find( user.get('username') ).then(function(u){ user.setProperties(u); });
bootbox.alert(I18n.t("admin.user.delete_failed"));
});
};
var message = I18n.t("admin.user.delete_confirm");
var buttons = [{
const buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
}, {
"label": I18n.t('admin.user.delete_dont_block'),
"class": "btn",
"callback": function(){
performDestroy(false);
}
"callback": function(){ performDestroy(false); }
}, {
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.delete_and_block'),
"class": "btn btn-danger",
"callback": function(){
performDestroy(true);
}
"callback": function(){ performDestroy(true); }
}];
bootbox.dialog(message, buttons, {"classes": "delete-user-modal"});
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
},
deleteAsSpammer: function(successCallback) {
var user = this;
deleteAsSpammer(successCallback) {
const user = this;
user.checkEmail().then(function() {
var data = {
const data = {
posts: user.get('post_count'),
topics: user.get('topic_count'),
email: user.get('email') || I18n.t("flagging.hidden_email_address"),
ip_address: user.get('ip_address') || I18n.t("flagging.ip_address_missing")
};
var message = I18n.t('flagging.delete_confirm', data);
var buttons = [{
};
const message = I18n.t('flagging.delete_confirm', data),
buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel-inline",
"link": true
@ -461,7 +437,7 @@ Discourse.AdminUser = Discourse.User.extend({
"label": '<i class="fa fa-exclamation-triangle"></i> ' + I18n.t("flagging.yes_delete_spammer"),
"class": "btn btn-danger",
"callback": function() {
Discourse.ajax("/admin/users/" + user.get('id') + '.json', {
return Discourse.ajax("/admin/users/" + user.get('id') + '.json', {
type: 'DELETE',
data: {
delete_posts: true,
@ -477,23 +453,24 @@ Discourse.AdminUser = Discourse.User.extend({
} else {
bootbox.alert(I18n.t("admin.user.delete_failed"));
}
}, function() {
}).catch(function() {
bootbox.alert(I18n.t("admin.user.delete_failed"));
});
}
}];
bootbox.dialog(message, buttons, {"classes": "flagging-delete-spammer"});
});
},
loadDetails: function() {
var model = this;
if (model.get('loadedDetails')) { return Ember.RSVP.resolve(model); }
loadDetails() {
const user = this;
return Discourse.AdminUser.find(model.get('username_lower')).then(function (result) {
model.setProperties(result);
model.set('loadedDetails', true);
if (user.get('loadedDetails')) { return Ember.RSVP.resolve(user); }
return Discourse.AdminUser.find(user.get('username_lower')).then(function (result) {
user.setProperties(result);
user.set('loadedDetails', true);
});
},
@ -517,29 +494,25 @@ Discourse.AdminUser = Discourse.User.extend({
});
Discourse.AdminUser.reopenClass({
AdminUser.reopenClass({
bulkApprove: function(users) {
bulkApprove(users) {
_.each(users, function(user) {
user.set('approved', true);
user.set('can_approve', false);
return user.set('selected', false);
user.setProperties({
approved: true,
can_approve: false,
selected: false
});
});
bootbox.alert(I18n.t("admin.user.approve_bulk_success"));
return Discourse.ajax("/admin/users/approve-bulk", {
type: 'PUT',
data: {
users: users.map(function(u) {
return u.id;
})
}
});
data: { users: users.map((u) => u.id) }
}).finally(() => bootbox.alert(I18n.t("admin.user.approve_bulk_success")));
},
bulkReject: function(users) {
_.each(users, function(user){
bulkReject(users) {
_.each(users, function(user) {
user.set('can_approve', false);
user.set('selected', false);
});
@ -547,26 +520,26 @@ Discourse.AdminUser.reopenClass({
return Discourse.ajax("/admin/users/reject-bulk", {
type: 'DELETE',
data: {
users: users.map(function(u) { return u.id; }),
users: users.map((u) => u.id),
context: window.location.pathname
}
});
},
find: function(username) {
find(username) {
return Discourse.ajax("/admin/users/" + username + ".json").then(function (result) {
result.loadedDetails = true;
return Discourse.AdminUser.create(result);
});
},
findAll: function(query, filter) {
findAll(query, filter) {
return Discourse.ajax("/admin/users/list/" + query + ".json", {
data: filter
}).then(function(users) {
return users.map(function(u) {
return Discourse.AdminUser.create(u);
});
return users.map((u) => Discourse.AdminUser.create(u));
});
}
});
export default AdminUser;

View File

@ -45,7 +45,7 @@ export default Discourse.Route.extend({
actions: {
startBackup() {
showModal('admin_start_backup');
showModal('modals/admin-start-backup');
this.controllerFor('modal').set('modalClass', 'start-backup-modal');
},

View File

@ -25,7 +25,7 @@ export default Ember.Route.extend({
editGroupings() {
const groupings = this.controllerFor('admin-badges').get('badgeGroupings');
showModal('admin_edit_badge_groupings', groupings);
showModal('modals/admin-edit-badge-groupings', groupings);
},
preview(badge, explain) {
@ -40,7 +40,7 @@ export default Ember.Route.extend({
}
}).then(function(json) {
badge.set('preview_loading', false);
showModal('admin_badge_preview', json);
showModal('modals/admin-badge-preview', json);
}).catch(function(error) {
badge.set('preview_loading', false);
Em.Logger.error(error);

View File

@ -13,12 +13,12 @@ export default Discourse.Route.extend({
actions: {
showAgreeFlagModal(flaggedPost) {
showModal('admin_agree_flag', flaggedPost);
showModal('modals/admin-agree-flag', flaggedPost);
this.controllerFor('modal').set('modalClass', 'agree-flag-modal');
},
showDeleteFlagModal(flaggedPost) {
showModal('admin_delete_flag', flaggedPost);
showModal('modals/admin-delete-flag', flaggedPost);
this.controllerFor('modal').set('modalClass', 'delete-flag-modal');
}

View File

@ -13,12 +13,13 @@ export default Discourse.Route.extend({
actions: {
showDetailsModal(logRecord) {
showModal('admin_staff_action_log_details', logRecord);
showModal('modals/admin-staff-action-log-details', logRecord);
this.controllerFor('modal').set('modalClass', 'log-details-modal');
},
showCustomDetailsModal(logRecord) {
showModal(logRecord.action_name + '_details', logRecord);
const modalName = "modals/" + (logRecord.action_name + '_details').replace("_", "-");
showModal(modalName, logRecord);
this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal');
}
}

View File

@ -25,7 +25,7 @@ export default Discourse.Route.extend({
actions: {
showSuspendModal(user) {
showModal('admin_suspend_user', user);
showModal('modals/admin-suspend-user', user);
this.controllerFor('modal').set('modalClass', 'suspend-user-modal');
}
}

View File

@ -1,6 +1,6 @@
{{#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-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom.label'}}
{{admin-nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic.label'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -3,20 +3,20 @@
<div class='user-controls'>
{{#if active}}
{{#link-to 'user' model class="btn"}}
<i class='fa fa-user'></i>
{{fa-icon "user"}}
{{i18n 'admin.user.show_public_profile'}}
{{/link-to}}
{{#if can_impersonate}}
<button class='btn btn-danger' {{action "impersonate" target="content"}} title="{{i18n 'admin.impersonate.help'}}">
<i class='fa fa-crosshairs'></i>
{{i18n 'admin.impersonate.title'}}
</button>
<button class='btn btn-danger' {{action "impersonate" target="content"}} title="{{i18n 'admin.impersonate.help'}}">
{{fa-icon "crosshairs"}}
{{i18n 'admin.impersonate.title'}}
</button>
{{/if}}
{{#if currentUser.admin}}
<button class='btn' {{action "log_out" target="content"}}>
<i class='fa fa-power-off'></i>
{{i18n 'admin.user.log_out'}}
</button>
<button class='btn' {{action "log_out" target="content"}}>
{{fa-icon "power-off"}}
{{i18n 'admin.user.log_out'}}
</button>
{{/if}}
{{/if}}
</div>
@ -26,7 +26,7 @@
<div class='value'>{{username}}</div>
<div class='controls'>
{{#link-to 'preferences.username' model class="btn"}}
<i class='fa fa-pencil'></i>
{{fa-icon "pencil"}}
{{i18n 'user.change_username.title'}}
{{/link-to}}
</div>
@ -75,28 +75,32 @@
</div>
<div class='controls'>
{{#if editingTitle}}
<button class='btn' {{action "saveTitle"}}>{{i18n 'admin.user.save_title'}}</button>
<a href="#" {{action "toggleTitleEdit"}}>{{i18n 'cancel'}}</a>
{{d-button action="saveTitle" label="admin.user.save_title"}}
<a href {{action "toggleTitleEdit"}}>{{i18n 'cancel'}}</a>
{{else}}
<button class='btn' {{action "toggleTitleEdit"}}><i class="fa fa-pencil"></i>{{i18n 'admin.user.edit_title'}}</button>
{{d-button action="toggleTitleEdit" icon="pencil" label="admin.user.edit_title"}}
{{/if}}
</div>
</div>
{{#if currentUser.admin}}
<div class='display-row'>
<div class='field'>{{i18n 'admin.groups.title'}}</div>
<div class='field'>{{i18n 'admin.groups.automatic.title'}}</div>
<div class='value'>{{automaticGroups}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.groups.custom.title'}}</div>
<div class='value'>
{{admin-group-selector selected=model.groups available=availableGroups}}
{{admin-group-selector selected=customGroups available=availableGroups}}
</div>
<div class='controls'>
{{#if custom_groups}}
{{#if customGroups}}
{{i18n 'admin.groups.primary'}}
{{combo-box content=custom_groups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
{{combo-box content=customGroups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
{{/if}}
{{#if primaryGroupDirty}}
<button class='btn ok no-text' {{action "savePrimaryGroup"}}><i class='fa fa-check'></i></button>
<button class='btn cancel no-text' {{action "resetPrimaryGroup"}}><i class='fa fa-times'></i></button>
{{d-button icon="check" class="ok no-text" action="savePrimaryGroup"}}
{{d-button icon="times" class="cancel no-text" action="resetPrimaryGroup"}}
{{/if}}
</div>
</div>
@ -132,7 +136,7 @@
{{i18n 'badges.badge_count' count=badge_count}}
</div>
<div class='controls'>
{{#link-to 'adminUser.badges' this class="btn"}}<i class="fa fa-certificate"></i>{{i18n 'admin.badges.edit_badges'}}{{/link-to}}
{{#link-to 'adminUser.badges' this class="btn"}}{{fa-icon "certificate"}}{{i18n 'admin.badges.edit_badges'}}{{/link-to}}
</div>
</div>
{{/if}}
@ -165,13 +169,11 @@
<div class='value'>
{{#if approved}}
{{i18n 'admin.user.approved_by'}}
{{#link-to 'adminUser' approvedBy}}{{avatar approvedBy imageSize="small"}}{{/link-to}}
{{#link-to 'adminUser' approvedBy}}{{approvedBy.username}}{{/link-to}}
{{else}}
{{i18n 'no_value'}}
{{/if}}
</div>
<div class='controls'>
{{#if approved}}
@ -179,7 +181,7 @@
{{else}}
{{#if can_approve}}
<button class='btn' {{action "approve" target="content"}}>
<i class='fa fa-check'></i>
{{fa-icon "check"}}
{{i18n 'admin.user.approve'}}
</button>
{{/if}}
@ -206,13 +208,13 @@
{{else}}
{{#if can_send_activation_email}}
<button class='btn' {{action "sendActivationEmail" target="content"}}>
<i class='fa fa-envelope'></i>
{{fa-icon "envelope"}}
{{i18n 'admin.user.send_activation_email'}}
</button>
{{/if}}
{{#if can_activate}}
<button class='btn' {{action "activate" target="content"}}>
<i class='fa fa-check'></i>
{{fa-icon "check"}}
{{i18n 'admin.user.activate'}}
</button>
{{/if}}
@ -222,19 +224,18 @@
<div class='display-row'>
<div class='field'>{{i18n 'admin.api.key'}}</div>
{{#if api_key}}
<div class='long-value'>
{{api_key.key}}
<button class='btn' {{action "regenerateApiKey"}}><i class="fa fa-undo"></i>{{i18n 'admin.api.regenerate'}}</button>
<button {{action "revokeApiKey"}} class="btn"><i class="fa fa-times"></i>{{i18n 'admin.api.revoke'}}</button>
{{d-button action="regenerateApiKey" icon="undo" label="admin.api.regenerate"}}
{{d-button action="revokeApiKey" icon="times" label="admin.api.revoke"}}
</div>
{{else}}
<div class='value'>
&mdash;
&mdash;
</div>
<div class='controls'>
<button {{action "generateApiKey"}} class="btn"><i class="fa fa-key"></i>{{i18n 'admin.api.generate'}}</button>
{{d-button action="generateApiKey" icon="key" label="admin.api.generate"}}
</div>
{{/if}}
</div>
@ -245,37 +246,36 @@
<div class='controls'>
{{#if can_revoke_admin}}
<button class='btn' {{action "revokeAdmin" target="content"}}>
<i class='fa fa-shield'></i>
{{fa-icon "shield"}}
{{i18n 'admin.user.revoke_admin'}}
</button>
{{/if}}
{{#if can_grant_admin}}
<button class='btn' {{action "grantAdmin" target="content"}}>
<i class='fa fa-shield'></i>
{{fa-icon "shield"}}
{{i18n 'admin.user.grant_admin'}}
</button>
{{/if}}
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.moderator'}}</div>
<div class='value'>{{moderator}}</div>
<div class='controls'>
{{#if can_revoke_moderation}}
<button class='btn' {{action "revokeModeration" target="content"}}>
<i class='fa fa-shield'></i>
{{fa-icon "shield"}}
{{i18n 'admin.user.revoke_moderation'}}
</button>
{{/if}}
{{#if can_grant_moderation}}
<button class='btn' {{action "grantModeration" target="content"}}>
<i class='fa fa-shield'></i>
{{fa-icon "shield"}}
{{i18n 'admin.user.grant_moderation'}}
</button>
{{/if}}
</div>
</div>
<div class='display-row'>
@ -284,8 +284,8 @@
{{combo-box content=trustLevels value=trust_level nameProperty="detailedName"}}
{{#if dirty}}
<div>
<button class='btn ok no-text' {{action "saveTrustLevel" target="content"}}><i class='fa fa-check'></i></button>
<button class='btn cancel no-text' {{action "restoreTrustLevel" target="content"}}><i class='fa fa-times'></i></button>
<button class='btn ok no-text' {{action "saveTrustLevel" target="content"}}>{{fa-icon "check"}}</button>
<button class='btn cancel no-text' {{action "restoreTrustLevel" target="content"}}>{{fa-icon "times"}}</button>
</div>
{{/if}}
</div>
@ -300,7 +300,6 @@
{{#if tl3Requirements}}
{{#link-to 'adminUser.tl3Requirements' this class="btn"}}{{i18n 'admin.user.trust_level_3_requirements'}}{{/link-to}}
{{/if}}
</div>
</div>
@ -310,7 +309,7 @@
<div class='controls'>
{{#if isSuspended}}
<button class='btn btn-danger' {{action "unsuspend" target="content"}}>
<i class='fa fa-ban'></i>
{{fa-icon "ban"}}
{{i18n 'admin.user.unsuspend'}}
</button>
{{suspendDuration}}
@ -318,7 +317,7 @@
{{else}}
{{#if canSuspend}}
<button class='btn btn-danger' {{action "showSuspendModal" this}}>
<i class='fa fa-ban'></i>
{{fa-icon "ban"}}
{{i18n 'admin.user.suspend'}}
</button>
{{i18n 'admin.user.suspended_explanation'}}
@ -328,17 +327,17 @@
</div>
{{#if isSuspended}}
<div class='display-row highlight-danger'>
<div class='field'>{{i18n 'admin.user.suspended_by'}}</div>
<div class='value'>
{{#link-to 'adminUser' suspendedBy}}{{avatar suspendedBy imageSize="tiny"}}{{/link-to}}
{{#link-to 'adminUser' suspendedBy}}{{suspendedBy.username}}{{/link-to}}
<div class='display-row highlight-danger'>
<div class='field'>{{i18n 'admin.user.suspended_by'}}</div>
<div class='value'>
{{#link-to 'adminUser' suspendedBy}}{{avatar suspendedBy imageSize="tiny"}}{{/link-to}}
{{#link-to 'adminUser' suspendedBy}}{{suspendedBy.username}}{{/link-to}}
</div>
<div class='controls'>
<b>{{i18n 'admin.user.suspend_reason'}}</b>:
{{suspend_reason}}
</div>
</div>
<div class='controls'>
<b>{{i18n 'admin.user.suspend_reason'}}</b>:
{{suspend_reason}}
</div>
</div>
{{/if}}
<div class='display-row' {{bind-attr class=":display-row blocked:highlight-danger"}}>
@ -347,7 +346,7 @@
<div class='controls'>
{{#if blocked}}
<button class='btn' {{action "unblock" target="content"}}>
<i class='fa fa-thumbs-o-up'></i>
{{fa-icon "thumbs-o-up"}}
{{i18n 'admin.user.unblock'}}
</button>
{{i18n 'admin.user.block_explanation'}}
@ -384,10 +383,12 @@
<div class='value'>{{post_count}}</div>
<div class='controls'>
{{#if can_delete_all_posts}}
<button class='btn btn-danger' {{action "deleteAllPosts" target="content"}}>
<i class='fa fa-trash-o'></i>
{{i18n 'admin.user.delete_all_posts'}}
</button>
{{#if post_count}}
<button class='btn btn-danger' {{action "deleteAllPosts" target="content"}}>
{{fa-icon "trash-o"}}
{{i18n 'admin.user.delete_all_posts'}}
</button>
{{/if}}
{{else}}
{{deleteAllPostsExplanation}}
{{/if}}
@ -454,7 +455,7 @@
{{#unless anonymizeForbidden}}
{{d-button label="admin.user.anonymize"
icon="exclamation-triangle"
class="btn btn-danger"
class="btn-danger"
disabled=anonymizeForbidden
action="anonymize"}}
{{/unless}}
@ -462,7 +463,7 @@
{{#unless deleteForbidden}}
{{d-button label="admin.user.delete"
icon="exclamation-triangle"
class="btn btn-danger"
class="btn-danger"
disabled=deleteForbidden
action="destroy"}}
{{/unless}}
@ -471,7 +472,9 @@
{{#if deleteExplanation}}
<div class="clearfix"></div>
<br/>
<div class="pull-right"><i class="fa fa-exclamation-triangle"></i> {{deleteExplanation}}</div>
<div class="pull-right">
{{fa-icon "exclamation-triangle"}} {{deleteExplanation}}
</div>
{{/if}}
</section>
<div class="clearfix"></div>

View File

@ -1,4 +1,6 @@
Discourse.AdminAgreeFlagView = Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_agree_flag',
title: I18n.t('admin.flags.agree_flag_modal_title')
});

View File

@ -1,5 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
Discourse.AdminBadgePreviewView = Discourse.ModalBodyView.extend({
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_badge_preview',
title: I18n.t('admin.badges.preview.modal_title')
});

View File

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_delete_flag',
title: I18n.t('admin.flags.delete_flag_modal_title')
});

View File

@ -1,5 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
Discourse.AdminEditBadgeGroupingsView = Discourse.ModalBodyView.extend({
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_edit_badge_groupings',
title: I18n.t('admin.badges.badge_groupings.modal_title')
});

View File

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/logs/details_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View File

@ -1,4 +1,6 @@
Discourse.AdminStartBackupView = Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_start_backup',
title: I18n.t('admin.backups.operations.backup.confirm')
});

View File

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_suspend_user',
title: I18n.t('admin.user.suspend_modal_title')
});

View File

@ -1,12 +0,0 @@
/**
A modal view for deleting a flag.
@class AdminDeleteFlagView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.AdminDeleteFlagView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_delete_flag',
title: I18n.t('admin.flags.delete_flag_modal_title')
});

View File

@ -1,12 +0,0 @@
/**
A modal view for details of a staff action log record in a modal.
@class AdminStaffActionLogDetailsView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.AdminStaffActionLogDetailsView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/logs/details_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View File

@ -1,12 +0,0 @@
/**
A modal view for suspending a user.
@class AdminSuspendUserView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.AdminSuspendUserView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_suspend_user',
title: I18n.t('admin.user.suspend_modal_title')
});

View File

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/logs/site_customization_change_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View File

@ -1,13 +0,0 @@
/**
A modal view for details of a staff action log record in a modal
for when a site customization is created or changed.
@class ChangeSiteCustomizationDetailsView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.ChangeSiteCustomizationDetailsView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/logs/site_customization_change_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View File

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/logs/site_customization_change_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View File

@ -1,13 +0,0 @@
/**
A modal view for details of a staff action log record in a modal
for when a site customization is deleted.
@class DeleteSiteCustomizationDetailsView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.DeleteSiteCustomizationDetailsView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/logs/site_customization_change_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View File

@ -1,31 +1,45 @@
const ADMIN_MODELS = ['plugin'];
export default Ember.Object.extend({
pathFor(type, id) {
let path = "/" + Ember.String.underscore(type + 's');
pathFor(store, type, findArgs) {
let path = "/" + Ember.String.underscore(store.pluralize(type));
if (ADMIN_MODELS.indexOf(type) !== -1) { path = "/admin/" + path; }
if (id) { path += "/" + id; }
if (findArgs) {
if (typeof findArgs === "object") {
const queryString = Object.keys(findArgs)
.reject(k => !findArgs[k])
.map(k => k + "=" + encodeURIComponent(findArgs[k]));
if (queryString.length) {
path += "?" + queryString.join('&');
}
} else {
// It's serializable as a string if not an object
path += "/" + findArgs;
}
}
return path;
},
findAll(store, type) {
return Discourse.ajax(this.pathFor(type));
return Discourse.ajax(this.pathFor(store, type));
},
find(store, type, id) {
return Discourse.ajax(this.pathFor(type, id));
find(store, type, findArgs) {
return Discourse.ajax(this.pathFor(store, type, findArgs));
},
update(store, type, id, attrs) {
const data = {};
data[Ember.String.underscore(type)] = attrs;
return Discourse.ajax(this.pathFor(type, id), { method: 'PUT', data });
return Discourse.ajax(this.pathFor(store, type, id), { method: 'PUT', data });
},
destroyRecord(store, type, record) {
return Discourse.ajax(this.pathFor(type, record.get('id')), { method: 'DELETE' });
return Discourse.ajax(this.pathFor(store, type, record.get('id')), { method: 'DELETE' });
}
});

View File

@ -1,19 +1,19 @@
import UploadMixin from 'discourse/mixins/upload';
export default Em.Component.extend(UploadMixin, {
type: 'avatar',
tagName: 'span',
imageIsNotASquare: false,
type: 'avatar',
uploadUrl: Discourse.computed.url('username', '/users/%@/preferences/user_image'),
uploadButtonText: function() {
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
return this.get("uploading") ?
I18n.t("uploading") :
I18n.t("user.change_avatar.upload_picture");
}.property("uploading"),
uploadDone: function(data) {
var self = this;
uploadDone(data) {
// display a warning whenever the image is not a square
this.set("imageIsNotASquare", data.result.width !== data.result.height);
@ -21,13 +21,13 @@ export default Em.Component.extend(UploadMixin, {
// indeed, the server gives us back the url to the file we've just uploaded
// often, this file is not a square, so we need to crop it properly
// this will also capture the first frame of animated avatars when they're not allowed
Discourse.Utilities.cropAvatar(data.result.url, data.files[0].type).then(function(avatarTemplate) {
self.set("uploadedAvatarTemplate", avatarTemplate);
Discourse.Utilities.cropAvatar(data.result.url, data.files[0].type).then(avatarTemplate => {
this.set("uploadedAvatarTemplate", avatarTemplate);
// indicates the users is using an uploaded avatar (must happen after cropping, otherwise
// we will attempt to load an invalid avatar and cache a redirect to old one, uploadedAvatarTemplate
// trumps over custom avatar upload id)
self.set("custom_avatar_upload_id", data.result.upload_id);
this.set("custom_avatar_upload_id", data.result.upload_id);
});
// the upload is now done

View File

@ -32,5 +32,6 @@ export default Ember.Component.extend({
click() {
this.sendAction("action", this.get("actionParam"));
return false;
}
});

View File

@ -0,0 +1,34 @@
import StringBuffer from 'discourse/mixins/string-buffer';
import { iconHTML } from 'discourse/helpers/fa-icon';
export default Ember.Component.extend(StringBuffer, {
tagName: 'th',
classNames: ['sortable'],
rerenderTriggers: ['order', 'asc'],
renderString(buffer) {
const icon = this.get('icon');
if (icon) {
buffer.push(iconHTML(icon));
}
const field = this.get('field');
buffer.push(I18n.t('directory.' + field));
if (field === this.get('order')) {
buffer.push(iconHTML(this.get('asc') ? 'chevron-up' : 'chevron-down'));
}
},
click() {
const currentOrder = this.get('order'),
field = this.get('field');
if (currentOrder === field) {
this.set('asc', this.get('asc') ? null : true);
} else {
this.setProperties({ order: field, asc: null });
}
}
});

View File

@ -10,9 +10,9 @@ export default Ember.Component.extend(CleansUp, {
},
_clickToClose: function() {
var self = this;
const self = this;
$('html').off('mousedown.top-period').on('mousedown.top-period', function(e) {
var $target = $(e.target);
const $target = $(e.target);
if (($target.prop('id') === 'topic-entrance') || (self.$().has($target).length !== 0)) {
return;
}
@ -20,12 +20,23 @@ export default Ember.Component.extend(CleansUp, {
});
},
click: function() {
click(e) {
if ($(e.target).closest('.period-popup').length) { return; }
if (!this.get('showPeriods')) {
var $chevron = this.$('i.fa-caret-down');
const $chevron = this.$('i.fa-caret-down');
this.$('#period-popup').css($chevron.position());
this.set('showPeriods', true);
this._clickToClose();
}
},
actions: {
changePeriod(p) {
this.cleanUp();
this.set('period', p);
this.sendAction('action', p);
}
}
});

View File

@ -1,21 +1,22 @@
var PosterNameComponent = Em.Component.extend({
const PosterNameComponent = Em.Component.extend({
classNames: ['names', 'trigger-user-card'],
displayNameOnPosts: Discourse.computed.setting('display_name_on_posts'),
// sanitize name for comparison
sanitizeName: function(name){
sanitizeName(name){
return name.toLowerCase().replace(/[\s_-]/g,'');
},
render: function(buffer) {
var post = this.get('post');
render(buffer) {
const post = this.get('post');
if (post) {
var name = post.get('name'),
username = post.get('username'),
linkClass = 'username',
primaryGroupName = post.get('primary_group_name'),
url = post.get('usernameUrl');
const username = post.get('username'),
primaryGroupName = post.get('primary_group_name'),
url = post.get('usernameUrl');
var linkClass = 'username',
name = post.get('name');
if (post.get('staff')) { linkClass += ' staff'; }
if (post.get('admin')) { linkClass += ' admin'; }
@ -29,7 +30,7 @@ var PosterNameComponent = Em.Component.extend({
buffer.push("<span class='" + linkClass + "'><a href='" + url + "' data-auto-route='true' data-user-card='" + username + "'>" + username + "</a>");
// Add a glyph if we have one
var glyph = this.posterGlyph(post);
const glyph = this.posterGlyph(post);
if (!Em.isEmpty(glyph)) {
buffer.push(glyph);
}
@ -42,7 +43,7 @@ var PosterNameComponent = Em.Component.extend({
}
// User titles
var title = post.get('user_title');
let title = post.get('user_title');
if (!Em.isEmpty(title)) {
title = Handlebars.Utils.escapeExpression(title);
@ -59,18 +60,10 @@ var PosterNameComponent = Em.Component.extend({
}
},
/**
Overwrite this to give a user a custom font awesome glyph.
@method posterGlyph
@param {Post} the related post.
@return {String} the glyph to render (or null for none)
**/
posterGlyph: function(post) {
var desc;
// Overwrite this to give a user a custom font awesome glyph.
posterGlyph(post) {
if(post.get('moderator')) {
desc = I18n.t('user.moderator_tooltip');
const desc = I18n.t('user.moderator_tooltip');
return '<i class="fa fa-shield" title="' + desc + '" alt="' + desc + '"></i>';
}
}

View File

@ -1,3 +1,14 @@
export default Ember.Component.extend({
classNames: ['top-title-buttons']
classNames: ['top-title-buttons'],
periods: function() {
const period = this.get('period');
return this.site.get('periods').filter(p => p !== period);
}.property('period'),
actions: {
changePeriod(p) {
this.sendAction('action', p);
}
}
});

View File

@ -1,13 +0,0 @@
import TopTitle from 'discourse/components/top-title';
export default TopTitle.extend({
tagName: 'button',
classNameBindings: [':btn', ':btn-default', 'unless:hidden'],
click: function() {
var url = this.get('period.showMoreUrl');
if (url) {
Discourse.URL.routeTo(url);
}
}
});

View File

@ -1,10 +0,0 @@
import StringBuffer from 'discourse/mixins/string-buffer';
export default Ember.Component.extend(StringBuffer, {
tagName: 'h2',
rerenderTriggers: ['period.title'],
renderString: function(buffer) {
buffer.push("<i class='fa fa-calendar-o'></i> " + this.get('period.title'));
}
});

View File

@ -1,3 +1,13 @@
export default Ember.Component.extend({
classNames: ['user-small']
classNames: ['user-small'],
userPath: Discourse.computed.url('username', '/users/%@'),
name: function() {
const name = this.get('user.name');
if (name && this.get('user.username') !== name) {
return name;
}
}.property('user.name')
});

View File

@ -2,8 +2,10 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality';
import DiscourseController from 'discourse/controllers/controller';
export default DiscourseController.extend(ModalFunctionality, {
uploadedAvatarTemplate: null,
hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'),
selectedUploadId: function(){
selectedUploadId: function() {
switch (this.get("selected")) {
case "system": return this.get("system_avatar_upload_id");
case "gravatar": return this.get("gravatar_avatar_upload_id");
@ -12,18 +14,16 @@ export default DiscourseController.extend(ModalFunctionality, {
}.property('selected', 'system_avatar_upload_id', 'gravatar_avatar_upload_id', 'custom_avatar_upload_id'),
actions: {
useUploadedAvatar: function() { this.set("selected", "uploaded"); },
useGravatar: function() { this.set("selected", "gravatar"); },
useSystem: function() { this.set("selected", "system"); },
refreshGravatar: function() {
var self = this;
self.set("gravatarRefreshDisabled", true);
Discourse
.ajax("/user_avatar/" + this.get("username") + "/refresh_gravatar", {method: 'POST'})
.then(function(result){
self.set("gravatarRefreshDisabled", false);
self.set("gravatar_avatar_upload_id", result.upload_id);
});
useUploadedAvatar() { this.set("selected", "uploaded"); },
useGravatar() { this.set("selected", "gravatar"); },
useSystem() { this.set("selected", "system"); },
refreshGravatar() {
this.set("gravatarRefreshDisabled", true);
return Discourse
.ajax("/user_avatar/" + this.get("username") + "/refresh_gravatar.json", { method: 'POST' })
.then(result => this.set("gravatar_avatar_upload_id", result.upload_id))
.finally(() => this.set("gravatarRefreshDisabled", false));
}
}

View File

@ -5,10 +5,9 @@ export default Ember.ArrayController.extend({
// Whether we've checked our messages
checkedMessages: false,
init() {
this._super();
_init: function() {
this.reset();
},
}.on("init"),
actions: {
closeMessage(message) {
@ -29,14 +28,11 @@ export default Ember.ArrayController.extend({
this.pushObject(message);
messagesByTemplate[templateName] = message;
}
},
}
},
/**
Resets all active messages. For example if composing a new post.
@method reset
**/
// Resets all active messages.
// For example if composing a new post.
reset() {
this.clear();
this.setProperties({
@ -46,42 +42,22 @@ export default Ember.ArrayController.extend({
});
},
/**
Called after the user has typed a reply. Some messages only get shown after being
typed.
@method typedReply
**/
// Called after the user has typed a reply.
// Some messages only get shown after being typed.
typedReply() {
var self = this;
this.get('queuedForTyping').forEach(function(msg){
if(self.popup){
self.popup(msg);
}
});
this.get('queuedForTyping').forEach(msg => this.send("popup", msg));
},
/**
Figure out if there are any messages that should be displayed above the composer.
@method queryFor
@params {Discourse.Composer} composer The composer model
**/
// Figure out if there are any messages that should be displayed above the composer.
queryFor(composer) {
if (this.get('checkedMessages')) { return; }
const self = this;
let queuedForTyping = self.get('queuedForTyping');
var queuedForTyping = self.get('queuedForTyping');
Discourse.ComposerMessage.find(composer).then(function (messages) {
Discourse.ComposerMessage.find(composer).then(messages => {
self.set('checkedMessages', true);
messages.forEach(function (msg) {
if (msg.wait_for_typing) {
queuedForTyping.addObject(msg);
} else {
self.popup(msg);
}
});
messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : self.send("popup", msg));
});
}

View File

@ -211,13 +211,13 @@ export default DiscourseController.extend({
}
}
var staged = false,
disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
var promise = composer.save({
var staged = false;
const disableJumpReply = Discourse.User.currentProp('disable_jump_reply');
const promise = composer.save({
imageSizes: this.get('view').imageSizes(),
editReason: this.get("editReason")
}).then(function(opts) {
// If we replied as a new topic successfully, remove the draft.
if (self.get('replyAsNewTopicDraft')) {
self.destroyDraft();
@ -240,34 +240,35 @@ export default DiscourseController.extend({
Discourse.URL.routeTo(opts.post.get('url'));
}
}
}, function(error) {
}).catch(function(error) {
composer.set('disableDrafts', false);
bootbox.alert(error);
});
if ( this.get('controllers.application.currentRouteName').split('.')[0] === 'topic' &&
composer.get('topic.id') === this.get('controllers.topic.model.id') ) {
if (this.get('controllers.application.currentRouteName').split('.')[0] === 'topic' &&
composer.get('topic.id') === this.get('controllers.topic.model.id')) {
staged = composer.get('stagedPost');
}
Em.run.schedule('afterRender', function() {
if (staged && !disableJumpReply) {
var postNumber = staged.get('post_number');
Discourse.URL.jumpToPost(postNumber, {skipIfOnScreen: true});
const postNumber = staged.get('post_number');
Discourse.URL.jumpToPost(postNumber, { skipIfOnScreen: true });
self.appEvents.trigger('post:highlight', postNumber);
}
});
this.messageBus.pause();
promise.finally(function(){
self.messageBus.resume();
});
return promise;
},
/**
Checks to see if a reply has been typed. This is signaled by a keyUp
event in a view.
@method checkReplyLength
**/
// Checks to see if a reply has been typed.
// This is signaled by a keyUp event in a view.
checkReplyLength() {
if (this.present('model.reply')) {
// Notify the composer messages controller that a reply has been typed. Some
@ -276,12 +277,8 @@ export default DiscourseController.extend({
}
},
/**
Fired after a user stops typing. Considers whether to check for similar
topics based on the current composer state.
@method findSimilarTopics
**/
// Fired after a user stops typing.
// Considers whether to check for similar topics based on the current composer state.
findSimilarTopics() {
// We don't care about similar topics unless creating a topic
if (!this.get('model.creatingTopic')) { return; }

View File

@ -0,0 +1,3 @@
export default Ember.Controller.extend({
me: Discourse.computed.propertyEqual('model.user.id', 'currentUser.id')
});

View File

@ -1,5 +1,4 @@
import ObjectController from 'discourse/controllers/object';
import TopPeriod from 'discourse/models/top-period';
export default ObjectController.extend({
needs: ['navigation/category', 'discovery/topics', 'application'],
@ -15,7 +14,7 @@ export default ObjectController.extend({
}.observes("loadedAllItems"),
showMoreUrl(period) {
var url = '', category = this.get('category');
let url = '', category = this.get('category');
if (category) {
url = '/c/' + Discourse.Category.slugFor(category) + (this.get('noSubcategories') ? '/none' : '') + '/l';
}
@ -23,15 +22,10 @@ export default ObjectController.extend({
return url;
},
periods: function() {
const self = this,
periods = [];
this.site.get('periods').forEach(function(p) {
periods.pushObject(TopPeriod.create({ id: p,
showMoreUrl: self.showMoreUrl(p),
periods }));
});
return periods;
}.property('category', 'noSubcategories'),
actions: {
changePeriod(p) {
Discourse.URL.routeTo(this.showMoreUrl(p));
}
}
});

View File

@ -0,0 +1,74 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
import { categoryLinkHTML } from 'discourse/helpers/category-link';
export default ObjectController.extend(ModalFunctionality, {
needs: ["topic"],
loading: true,
pinnedInCategoryCount: 0,
pinnedGloballyCount: 0,
bannerCount: 0,
categoryLink: function() {
return categoryLinkHTML(this.get("category"), { allowUncategorized: true });
}.property("category"),
unPinMessage: function() {
return this.get("pinned_globally") ?
I18n.t("topic.feature_topic.unpin_globally") :
I18n.t("topic.feature_topic.unpin", { categoryLink: this.get("categoryLink") });
}.property("categoryLink", "pinned_globally"),
pinMessage: function() {
return I18n.t("topic.feature_topic.pin", { categoryLink: this.get("categoryLink") });
}.property("categoryLink"),
alreadyPinnedMessage: function() {
return I18n.t("topic.feature_topic.already_pinned", { categoryLink: this.get("categoryLink"), count: this.get("pinnedInCategoryCount") });
}.property("categoryLink", "pinnedInCategoryCount"),
onShow() {
this.set("loading", true);
return Discourse.ajax("/topics/feature_stats.json", {
data: { category_id: this.get("category.id") }
}).then(result => {
if (result) {
this.setProperties({
pinnedInCategoryCount: result.pinned_in_category_count,
pinnedGloballyCount: result.pinned_globally_count,
bannerCount: result.banner_count,
});
}
}).finally(() => this.set("loading", false));
},
_forwardAction(name) {
this.get("controllers.topic").send(name);
this.send("closeModal");
},
_confirmBeforePinning(count, name, action) {
if (count < 4) {
this._forwardAction(action);
} else {
this.send("hideModal");
bootbox.confirm(
I18n.t("topic.feature_topic.confirm_" + name, { count: count }),
I18n.t("no_value"),
I18n.t("yes_value"),
confirmed => confirmed ? this._forwardAction(action) : this.send("reopenModal")
);
}
},
actions: {
pin() { this._forwardAction("togglePinned"); },
pinGlobally() { this._confirmBeforePinning(this.get("pinnedGloballyCount"), "pin_globally", "pinGlobally"); },
unpin() { this._forwardAction("togglePinned"); },
makeBanner() { this._forwardAction("makeBanner"); },
removeBanner() { this._forwardAction("removeBanner"); },
}
});

View File

@ -21,6 +21,7 @@ export default ObjectController.extend(ModalFunctionality, {
if (this.get('saving')) return true;
if (this.blank('email')) return true;
if (!Discourse.Utilities.emailValid(this.get('email'))) return true;
if (this.get('model.details.can_invite_to')) return false;
if (this.get('isPrivateTopic') && this.blank('groupNames')) return true;
return false;
}.property('email', 'isPrivateTopic', 'groupNames', 'saving'),

View File

@ -19,12 +19,17 @@ export default ObjectController.extend(CanCheckEmails, {
newNameInput: null,
userFields: function() {
var siteUserFields = this.site.get('user_fields');
let siteUserFields = this.site.get('user_fields');
if (!Ember.isEmpty(siteUserFields)) {
var userFields = this.get('user_fields');
return siteUserFields.filterProperty('editable', true).sortBy('field_type').map(function(uf) {
var val = userFields ? userFields[uf.get('id').toString()] : null;
return Ember.Object.create({value: val, field: uf});
const userFields = this.get('user_fields');
// Staff can edit fields that are not `editable`
if (!this.get('currentUser.staff')) {
siteUserFields = siteUserFields.filterProperty('editable', true);
}
return siteUserFields.sortBy('field_type').map(function(field) {
const value = userFields ? userFields[field.get('id').toString()] : null;
return Ember.Object.create({ value, field });
});
}
}.property('user_fields.@each.value'),
@ -82,16 +87,16 @@ export default ObjectController.extend(CanCheckEmails, {
actions: {
save: function() {
var self = this;
save() {
const self = this;
this.setProperties({ saving: true, saved: false });
var model = this.get('model'),
const model = this.get('model'),
userFields = this.get('userFields');
// Update the user fields
if (!Ember.isEmpty(userFields)) {
var modelFields = model.get('user_fields');
const modelFields = model.get('user_fields');
if (!Ember.isEmpty(modelFields)) {
userFields.forEach(function(uf) {
modelFields[uf.get('field.id').toString()] = uf.get('value');
@ -120,8 +125,8 @@ export default ObjectController.extend(CanCheckEmails, {
});
},
changePassword: function() {
var self = this;
changePassword() {
const self = this;
if (!this.get('passwordProgress')) {
this.set('passwordProgress', I18n.t("user.change_password.in_progress"));
return this.get('model').changePassword().then(function() {
@ -140,32 +145,31 @@ export default ObjectController.extend(CanCheckEmails, {
}
},
delete: function() {
delete() {
this.set('deleting', true);
var self = this,
const self = this,
message = I18n.t('user.delete_account_confirm'),
model = this.get('model'),
buttons = [{
"label": I18n.t("cancel"),
"class": "cancel-inline",
"link": true,
"callback": function() {
self.set('deleting', false);
}
}, {
"label": '<i class="fa fa-exclamation-triangle"></i> ' + I18n.t("user.delete_account"),
"class": "btn btn-danger",
"callback": function() {
model.delete().then(function() {
bootbox.alert(I18n.t('user.deleted_yourself'), function() {
window.location.pathname = Discourse.getURL('/');
});
}, function() {
bootbox.alert(I18n.t('user.delete_yourself_not_allowed'));
self.set('deleting', false);
});
}
}];
buttons = [
{ label: I18n.t("cancel"),
class: "cancel-inline",
link: true,
callback: () => { this.set('deleting', false); }
},
{ label: '<i class="fa fa-exclamation-triangle"></i> ' + I18n.t("user.delete_account"),
class: "btn btn-danger",
callback() {
model.delete().then(function() {
bootbox.alert(I18n.t('user.deleted_yourself'), function() {
window.location.pathname = Discourse.getURL('/');
});
}, function() {
bootbox.alert(I18n.t('user.delete_yourself_not_allowed'));
self.set('deleting', false);
});
}
}
];
bootbox.dialog(message, buttons, {"classes": "delete-account"});
}
}

View File

@ -13,16 +13,14 @@ export default DiscourseController.extend({
if (this.blank('buffer')) this.set('post', null);
}.observes('buffer'),
/**
Save the currently selected text and displays the
"quote reply" button
**/
// Save the currently selected text and displays the
// "quote reply" button
selectText(postId) {
// anonymous users cannot "quote-reply"
if (!Discourse.User.current()) return;
if (!this.currentUser) return;
// don't display the "quote-reply" button if we can't create a post
if (!this.get('controllers.topic.model.details.can_create_post')) return;
// don't display the "quote-reply" button if we can't at least reply as a new topic
if (!this.get('controllers.topic.model.details.can_reply_as_new_topic')) return;
const selection = window.getSelection();
// no selections
@ -85,7 +83,15 @@ export default DiscourseController.extend({
},
quoteText() {
const post = this.get('post');
// If we can't create a post, delegate to reply as new topic
if (!this.get('controllers.topic.model.details.can_create_post')) {
this.get('controllers.topic').send('replyAsNewTopic', post);
return;
}
const composerController = this.get('controllers.composer');
const composerOpts = {
action: Discourse.Composer.REPLY,

View File

@ -4,6 +4,7 @@ import ObjectController from 'discourse/controllers/object';
export default ObjectController.extend({
menuVisible: false,
showRecover: Em.computed.and('deleted', 'details.can_recover'),
isFeatured: Em.computed.or("pinned_at", "isBanner"),
actions: {
show: function() { this.set('menuVisible', true); },

View File

@ -94,6 +94,19 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
this.set('selectedReplies', []);
}.on('init'),
_togglePinnedStates(property) {
const value = this.get('pinned_at') ? false : true,
topic = this.get('content');
// optimistic update
topic.setProperties({
pinned_at: value,
pinned_globally: value
});
return topic.saveStatus(property, value);
},
actions: {
deleteTopic() {
this.deleteTopic();
@ -352,13 +365,28 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
},
togglePinned() {
// Note that this is different than clearPin
this.get('content').setStatus('pinned', this.get('pinned_at') ? false : true);
const value = this.get('pinned_at') ? false : true,
topic = this.get('content');
// optimistic update
topic.setProperties({
pinned_at: value ? moment() : null,
pinned_globally: false
});
return topic.saveStatus("pinned", value);
},
togglePinnedGlobally() {
// Note that this is different than clearPin
this.get('content').setStatus('pinned_globally', this.get('pinned_at') ? false : true);
pinGlobally() {
const topic = this.get('content');
// optimistic update
topic.setProperties({
pinned_at: moment(),
pinned_globally: true
});
return topic.saveStatus("pinned_globally", true);
},
toggleArchived() {

View File

@ -5,14 +5,16 @@ export default ObjectController.extend({
visible: false,
user: null,
username: null,
participant: null,
avatar: null,
userLoading: null,
cardTarget: null,
post: null,
// If inside a topic
topicPostCount: null,
postStream: Em.computed.alias('controllers.topic.postStream'),
enoughPostsForFiltering: Em.computed.gte('participant.post_count', 2),
enoughPostsForFiltering: Em.computed.gte('topicPostCount', 2),
viewingTopic: Em.computed.match('controllers.application.currentPath', /^topic\./),
viewingAdmin: Em.computed.match('controllers.application.currentPath', /^admin\./),
showFilter: Em.computed.and('viewingTopic', 'postStream.hasNoFilters', 'enoughPostsForFiltering'),
@ -28,14 +30,14 @@ export default ObjectController.extend({
}.property('user.badge_count', 'user.featured_user_badges.@each'),
hasCardBadgeImage: function() {
var img = this.get('user.card_badge.image');
const img = this.get('user.card_badge.image');
return img && img.indexOf('fa-') !== 0;
}.property('user.card_badge.image'),
show: function(username, postId, target) {
show(username, postId, target) {
// XSS protection (should be encapsulated)
username = username.toString().replace(/[^A-Za-z0-9_]/g, "");
var url = "/users/" + username;
const url = "/users/" + username;
// Don't show on mobile
if (Discourse.Mobile.mobileView) {
@ -43,7 +45,7 @@ export default ObjectController.extend({
return;
}
var currentUsername = this.get('username'),
const currentUsername = this.get('username'),
wasVisible = this.get('visible'),
post = this.get('viewingTopic') && postId ? this.get('controllers.topic.postStream').findLoadedPost(postId) : null;
@ -60,20 +62,21 @@ export default ObjectController.extend({
return;
}
this.set('participant', null);
// Retrieve their participants info
var participants = this.get('controllers.topic.details.participants');
if (participants) {
this.set('participant', participants.findBy('username', username));
}
this.set('topicPostCount', null);
this.setProperties({ user: null, userLoading: username, cardTarget: target });
var self = this;
return Discourse.User.findByUsername(username, { stats: false }).then(function(user) {
const args = { stats: false };
args.include_post_count_for = this.get('controllers.topic.id');
const self = this;
return Discourse.User.findByUsername(username, args).then(function(user) {
if (user.topic_post_count) {
self.set('topicPostCount', user.topic_post_count[args.include_post_count_for]);
}
user = Discourse.User.create(user);
self.setProperties({ user: user, avatar: user, visible: true});
self.setProperties({ user, avatar: user, visible: true});
self.appEvents.trigger('usercard:shown');
}).catch(function(error) {
self.close();
@ -83,19 +86,19 @@ export default ObjectController.extend({
});
},
close: function() {
close() {
this.setProperties({ visible: false, cardTarget: null });
},
actions: {
togglePosts: function(user) {
var postStream = this.get('controllers.topic.postStream');
togglePosts(user) {
const postStream = this.get('controllers.topic.postStream');
postStream.toggleParticipant(user.get('username'));
this.close();
},
cancelFilter: function() {
var postStream = this.get('postStream');
cancelFilter() {
const postStream = this.get('postStream');
postStream.cancelFilter();
postStream.refresh();
this.close();

View File

@ -0,0 +1,19 @@
export default Ember.Controller.extend({
queryParams: ['period', 'order', 'asc', 'name'],
period: 'weekly',
order: 'likes_received',
asc: null,
name: '',
showTimeRead: Ember.computed.equal('period', 'all'),
_setName: Discourse.debounce(function() {
this.set('name', this.get('nameInput'));
}, 500).observes('nameInput'),
actions: {
loadMore() {
this.get('model').loadMore();
}
}
});

View File

@ -2,13 +2,22 @@
Support for various code blocks
**/
var acceptableCodeClasses =
["auto", "1c", "actionscript", "apache", "applescript", "avrasm", "axapta", "bash", "brainfuck",
"clojure", "cmake", "coffeescript", "cpp", "cs", "css", "d", "delphi", "diff", "xml", "django", "dos",
"erlang-repl", "erlang", "glsl", "go", "handlebars", "haskell", "http", "ini", "java", "javascript",
"json", "lisp", "lua", "markdown", "matlab", "mel", "nginx", "nohighlight", "objectivec", "parser3",
"perl", "php", "profile", "python", "r", "rib", "rsl", "ruby", "rust", "scala", "smalltalk", "sql",
"tex", "text", "vala", "vbscript", "vhdl"];
var acceptableCodeClasses;
function init() {
acceptableCodeClasses = Discourse.SiteSettings.highlighted_languages.split("|");
if (Discourse.SiteSettings.highlighted_languages.length > 0) {
var regexpSource = "^lang-(" + "nohighlight|auto|" + Discourse.SiteSettings.highlighted_languages + ")$";
Discourse.Markdown.whiteListTag('code', 'class', new RegExp(regexpSource, "i"));
}
}
if (Discourse.SiteSettings && Discourse.SiteSettings.highlighted_languages) {
init();
} else {
Discourse.initializer({initialize: init, name: 'load-acceptable-code-classes'});
}
var textCodeClasses = ["text", "pre", "plain"];
@ -27,6 +36,7 @@ Discourse.Dialect.replaceBlock({
emitter: function(blockContents, matches) {
var klass = Discourse.SiteSettings.default_code_lang;
if (matches[1] && acceptableCodeClasses.indexOf(matches[1]) !== -1) {
klass = matches[1];
}
@ -69,6 +79,3 @@ Discourse.Dialect.on('parseNode', function (event) {
}
});
// Whitelist the language classes
var regexpSource = "^lang-(" + acceptableCodeClasses.join('|') + ")$";
Discourse.Markdown.whiteListTag('code', 'class', new RegExp(regexpSource, "i"));

View File

@ -167,6 +167,11 @@ function outdent(t) {
return t.replace(/^([ ]{4}|\t)/gm, "");
}
function removeEmptyLines(t) {
return t.replace(/^\n+/, "")
.replace(/\s+$/, "");
}
function hideBackslashEscapedCharacters(t) {
return t.replace(/\\\\/g, "\u1E800")
.replace(/\\`/g, "\u1E8001");
@ -186,14 +191,14 @@ function hoistCodeBlocksAndSpans(text) {
// <pre>...</pre> code blocks
text = text.replace(/(^\n*|\n\n)<pre>([\s\S]*?)<\/pre>/ig, function(_, before, content) {
var hash = md5(content);
hoisted[hash] = escape(showBackslashEscapedCharacters(content.trim()));
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
return before + "<pre>" + hash + "</pre>";
});
// fenced code blocks (AKA GitHub code blocks)
text = text.replace(/(^\n*|\n\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) {
var hash = md5(content);
hoisted[hash] = escape(showBackslashEscapedCharacters(content.trim()));
hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
return before + "```" + language + "\n" + hash + "\n```";
});
@ -209,9 +214,7 @@ function hoistCodeBlocksAndSpans(text) {
}
// we can safely hoist the code block
var hash = md5(content);
// only remove trailing whitespace
content = content.replace(/\s+$/, "");
hoisted[hash] = escape(outdent(showBackslashEscapedCharacters(content)));
hoisted[hash] = escape(outdent(showBackslashEscapedCharacters(removeEmptyLines(content))));
return before + " " + hash + "\n";
});
@ -275,7 +278,9 @@ Discourse.Dialect = {
var keys = Object.keys(hoisted);
if (keys.length) {
keys.forEach(function(key) {
result = result.replace(new RegExp(key, "g"), hoisted[key]);
result = result.replace(new RegExp(key, "g"), function() {
return hoisted[key];
});
});
}

View File

@ -0,0 +1,11 @@
import { iconHTML } from 'discourse/helpers/fa-icon';
const TITLE_SUBS = { yearly: 'this_year',
monthly: 'this_month',
daily: 'today',
all: 'all' };
export default Ember.Handlebars.makeBoundHelper(function (period) {
const title = I18n.t('filters.top.' + (TITLE_SUBS[period] || 'this_week'));
return new Handlebars.SafeString(iconHTML('calendar-o') + " " + title);
});

View File

@ -14,7 +14,7 @@ export default {
Sharing.addSource({
id: 'twitter',
iconClass: 'fa-twitter-square',
faIcon: 'fa-twitter-square',
generateUrl: function(link, title) {
return "http://twitter.com/intent/tweet?url=" + encodeURIComponent(link) + "&text=" + encodeURIComponent(title);
},
@ -24,7 +24,7 @@ export default {
Sharing.addSource({
id: 'facebook',
iconClass: 'fa-facebook-square',
faIcon: 'fa-facebook-square',
generateUrl: function(link, title) {
return "http://www.facebook.com/sharer.php?u=" + encodeURIComponent(link) + '&t=' + encodeURIComponent(title);
},
@ -33,7 +33,7 @@ export default {
Sharing.addSource({
id: 'google+',
iconClass: 'fa-google-plus-square',
faIcon: 'fa-google-plus-square',
generateUrl: function(link) {
return "https://plus.google.com/share?url=" + encodeURIComponent(link);
},
@ -43,7 +43,7 @@ export default {
Sharing.addSource({
id: 'email',
iconClass: 'fa-envelope-square',
faIcon: 'fa-envelope-square',
generateUrl: function(link, title) {
return "mailto:?to=&subject=" + encodeURIComponent('[' + Discourse.SiteSettings.title + '] ' + title) + "&body=" + encodeURIComponent(link);
}

View File

@ -2,11 +2,6 @@
Debounce a Javascript function. This means if it's called many times in a time limit it
should only be executed once (at the end of the limit counted from the last call made).
Original function will be called with the context and arguments from the last call made.
@method debounce
@module Discourse
@param {function} func The function to debounce
@param {Number} wait how long to wait
**/
Discourse.debounce = function(func, wait) {
var self, args;

View File

@ -4,6 +4,7 @@ Discourse.Emoji = {};
Discourse.Emoji.ImageVersion = "0"
var emoji = <%= Emoji.standard.map(&:name).flatten.inspect %>;
var aliases = <%= Emoji.aliases.inspect.gsub("=>", ":") %>;
var extendedEmoji = {};
Discourse.Dialect.registerEmoji = function(code, url) {
@ -19,13 +20,12 @@ Discourse.Emoji.list = function(){
var toSearch;
var search = function(term, options) {
Discourse.Emoji.search = function(term, options) {
var maxResults = (options && options["maxResults"]) || -1;
toSearch = toSearch || emoji.concat(Object.keys(extendedEmoji));
if (maxResults === 0) { return []; }
toSearch = toSearch || Discourse.Emoji.list();
var i, results = [];
var done = function() {
@ -51,12 +51,17 @@ var search = function(term, options) {
return results;
}
Discourse.Emoji.search = search;
var emojiHash = {};
// add all default emojis
emoji.forEach(function(code){ emojiHash[code] = true; });
// and their aliases
for (var name in aliases) {
aliases[name].forEach(function(alias) {
emojiHash[alias] = true;
});
}
var urlFor = function(code) {
Discourse.Emoji.urlFor = urlFor = function(code) {
var url, set = Discourse.SiteSettings.emoji_set;
code = code.toLowerCase();
@ -80,8 +85,6 @@ var urlFor = function(code) {
return url;
}
Discourse.Emoji.urlFor = urlFor;
Discourse.Emoji.exists = function(code){
code = code.toLowerCase();
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code));
@ -98,27 +101,27 @@ function imageFor(code) {
// Also support default emotions
var translations = {
':)' : 'smile',
':-)' : 'smile',
':-)' : 'smile',
':(' : 'frowning',
':-(' : 'frowning',
':-(' : 'frowning',
';)' : 'wink',
';-)' : 'wink',
';-)' : 'wink',
':\'(' : 'cry',
':\'-(' : 'cry',
':-\'(' : 'cry',
':\'-(': 'cry',
':-\'(': 'cry',
':p' : 'stuck_out_tongue',
':P' : 'stuck_out_tongue',
':-P' : 'stuck_out_tongue',
':-P' : 'stuck_out_tongue',
':O' : 'open_mouth',
':-O' : 'open_mouth',
':-O' : 'open_mouth',
':D' : 'smiley',
':-D' : 'smiley',
':-D' : 'smiley',
':|' : 'expressionless',
':-|' : 'expressionless',
':-|' : 'expressionless',
";P" : 'stuck_out_tongue_winking_eye',
";-P" : 'stuck_out_tongue_winking_eye',
";-P" : 'stuck_out_tongue_winking_eye',
":$" : 'blush',
":-$" : 'blush'
":-$" : 'blush'
};
Discourse.Emoji.translations = translations;

View File

@ -3,8 +3,12 @@
import loadScript from 'discourse/lib/load-script';
export default function highlightSyntax($elem) {
const selector = Discourse.SiteSettings.autohighlight_all_code ? 'pre code' : 'pre code[class]';
const selector = Discourse.SiteSettings.autohighlight_all_code ? 'pre code' : 'pre code[class]',
path = Discourse.HighlightJSPath;
if (!path) { return; }
$(selector, $elem).each(function(i, e) {
loadScript("/javascripts/highlight.pack.js").then(() => hljs.highlightBlock(e));
loadScript(path).then(() => hljs.highlightBlock(e));
});
}

View File

@ -0,0 +1,11 @@
export default function (element) {
if (element instanceof jQuery) { element = element[0]; }
const $window = $(window),
rect = element.getBoundingClientRect();
return rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= $window.height() &&
rect.right <= $window.width();
}

View File

@ -31,13 +31,20 @@ export default function loadScript(url, opts) {
resolve();
};
var cdnUrl = url;
// Scripts should always load from CDN
if (Discourse.CDN && url[0] === "/" && url[1] !== "/") {
cdnUrl = Discourse.CDN.replace(/\/$/,"") + url;
}
// Some javascript depends on the path of where it is loaded (ace editor)
// to dynamically load more JS. In that case, add the `scriptTag: true`
// option.
if (opts.scriptTag) {
loadWithTag(url, cb);
loadWithTag(cdnUrl, cb);
} else {
$.getScript(url).then(cb);
Discourse.ajax({url: cdnUrl, dataType: "script", cache: true}).then(cb);
}
});
}

View File

@ -0,0 +1,61 @@
function applicable() {
// CriOS is Chrome on iPad / iPhone, OPiOS is Opera (they need no patching)
// Dolphin has a wierd user agent, rest seem a bit nitch
return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
navigator.userAgent.match(/Safari/g) &&
!navigator.userAgent.match(/CriOS/g) &&
!navigator.userAgent.match(/OPiOS/g);
}
// per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810
function positioningWorkaround($fixedElement) {
if (!applicable()) {
return;
}
const fixedElement = $fixedElement[0];
var positioningHack = function(evt){
const self = this;
if (fixedElement.style.position !== 'absolute') {
evt.preventDefault();
fixedElement.style.position = 'absolute';
fixedElement.style.top = (window.scrollY + $('.d-header').height() + 10) + 'px';
}
var blurred = function() {
if (_.include($(document.activeElement).parents(), fixedElement)) {
// something in focus so skip
return;
}
fixedElement.style.position = '';
fixedElement.style.top = '';
self.removeEventListener('blur', blurred);
};
blurred = _.debounce(blurred, 300);
if (this !== document.activeElement) {
self.focus();
}
self.addEventListener('blur', blurred);
};
const checkForInputs = _.debounce(function(){
$fixedElement.find('input,textarea').each(function(){
if (!$(this).data('listening')) {
this.addEventListener('touchstart', positioningHack);
$(this).data('listening', true);
}
});
}, 100);
fixedElement.addEventListener('DOMNodeInserted', checkForInputs);
}
export default positioningWorkaround;

View File

@ -9,8 +9,10 @@
// This id must be present in the `share_links` site setting too
id: 'twitter',
// The icon that will be displayed
iconClass: 'fa-twitter-square',
// The icon that will be displayed, choose between font awesome class name `faIcon` and custom HTML `htmlIcon`.
// When both provided, prefer `faIcon`
faIcon: 'fa-twitter-square'
htmlIcon: '<img src="example.com/example.jpg">',
// A callback for generating the remote link from the `link` and `title`
generateUrl: function(link, title) {

View File

@ -1,20 +1,15 @@
export default function showModal(name, model) {
export default (name, model) => {
// We use the container here because modals are like singletons
// in Discourse. Only one can be shown with a particular state.
const route = Discourse.__container__.lookup('route:application');
route.controllerFor('modal').set('modalClass', null);
route.render(name, {into: 'modal', outlet: 'modalBody'});
route.render(name, { into: 'modal', outlet: 'modalBody' });
const controller = route.controllerFor(name);
if (controller) {
if (model) {
controller.set('model', model);
}
if (controller.onShow) {
controller.onShow();
}
if (model) { controller.set('model', model); }
if (controller.onShow) { controller.onShow(); }
controller.set('flashMessage', null);
}
return controller;
}
};

View File

@ -49,9 +49,11 @@ Discourse.Ajax = Em.Mixin.create({
var performAjax = function(resolve, reject) {
args.headers = args.headers || {};
if (_trackView && (!args.type || args.type === "GET")) {
_trackView = false;
args.headers = { 'Discourse-Track-View': true };
args.headers['Discourse-Track-View'] = true;
}
args.success = function(xhr) {
@ -80,6 +82,10 @@ Discourse.Ajax = Em.Mixin.create({
if (!args.type) args.type = 'GET';
if (!args.dataType && args.type.toUpperCase() === 'GET') args.dataType = 'json';
if (args.dataType === "script") {
args.headers['Discourse-Script'] = true;
}
if (args.type === 'GET' && args.cache !== true) {
args.cache = false;
}

View File

@ -1,14 +1,13 @@
export default Em.Mixin.create({
actions: {
didTransition: function() {
var self = this;
Em.run.schedule("afterRender", function() {
self.controllerFor("application").set("showFooter", true);
didTransition() {
Em.run.schedule("afterRender", () => {
this.controllerFor("application").set("showFooter", true);
});
return true;
},
willTransition: function() {
willTransition() {
this.controllerFor("application").set("showFooter", false);
return true;
}

View File

@ -1,33 +1,30 @@
export default Ember.Mixin.create({
_watchProps: function() {
var args = this.get('rerenderTriggers');
const args = this.get('rerenderTriggers');
if (!Ember.isNone(args)) {
var self = this;
args.forEach(function(k) {
self.addObserver(k, self.rerenderString);
});
args.forEach(k => this.addObserver(k, this.rerenderString));
}
}.on('init'),
render: function(buffer) {
render(buffer) {
this.renderString(buffer);
},
renderString: function(buffer){
var template = Discourse.__container__.lookup('template:' + this.rawTemplate);
renderString(buffer){
const template = Discourse.__container__.lookup('template:' + this.rawTemplate);
if (template) {
buffer.push(template(this));
}
},
_rerenderString: function() {
var buffer = [];
_rerenderString() {
const buffer = [];
this.renderString(buffer);
this.$().html(buffer.join(''));
},
rerenderString: function() {
rerenderString() {
Ember.run.once(this, '_rerenderString');
}

View File

@ -123,7 +123,6 @@ Discourse.Post = Discourse.Model.extend({
save: function(complete, error) {
var self = this;
if (!this.get('newPost')) {
// We're updating a post
return Discourse.ajax("/posts/" + (this.get('id')), {
type: 'PUT',
@ -137,13 +136,12 @@ Discourse.Post = Discourse.Model.extend({
self.set('version', result.post.version);
if (result.category) Discourse.Site.current().updateCategory(result.category);
if (complete) complete(Discourse.Post.create(result.post));
}, function(result) {
}).catch(function(result) {
// Post failed to update
if (error) error(result);
});
} else {
// We're saving a post
var data = this.getProperties(Discourse.Composer.serializedFieldsForCreate());
data.reply_to_post_number = this.get('reply_to_post_number');
@ -162,7 +160,7 @@ Discourse.Post = Discourse.Model.extend({
}).then(function(result) {
// Post created
if (complete) complete(Discourse.Post.create(result));
}, function(result) {
}).catch(function(result) {
// Failed to create a post
if (error) error(result);
});

View File

@ -40,6 +40,8 @@ const Composer = Discourse.Model.extend({
return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message';
}.property('creatingPrivateMessage', 'topic'),
topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'),
editingPost: Em.computed.equal('action', EDIT),
replyingToTopic: Em.computed.equal('action', REPLY),
@ -215,10 +217,13 @@ const Composer = Discourse.Model.extend({
minimumPostLength: function() {
if( this.get('privateMessage') ) {
return Discourse.SiteSettings.min_private_message_post_length;
} else if (this.get('topicFirstPost')) {
// first post (topic body)
return Discourse.SiteSettings.min_first_post_length;
} else {
return Discourse.SiteSettings.min_post_length;
}
}.property('privateMessage'),
}.property('privateMessage', 'topicFirstPost'),
/**
Computes the length of the title minus non-significant whitespaces
@ -385,7 +390,7 @@ const Composer = Discourse.Model.extend({
},
save(opts) {
if( !this.get('cantSubmitPost') ) {
if (!this.get('cantSubmitPost')) {
return this.get('editingPost') ? this.editPost(opts) : this.createPost(opts);
}
},
@ -409,8 +414,9 @@ const Composer = Discourse.Model.extend({
// When you edit a post
editPost(opts) {
const post = this.get('post'),
oldCooked = post.get('cooked'),
self = this;
oldCooked = post.get('cooked'),
self = this;
let promise;
// Update the title if we've changed it, otherwise consider it a
@ -418,7 +424,6 @@ const Composer = Discourse.Model.extend({
if (this.get('title') &&
post.get('post_number') === 1 &&
this.get('topic.details.can_edit')) {
const topicProps = this.getProperties(Object.keys(_edit_topic_serializer));
promise = Discourse.Topic.update(this.get('topic'), topicProps);
} else {
@ -431,33 +436,26 @@ const Composer = Discourse.Model.extend({
imageSizes: opts.imageSizes,
cooked: this.getCookedHtml()
});
this.set('composeState', CLOSED);
return promise.then(function() {
return post.save(function(result) {
post.updateFromPost(result);
self.clearState();
}).catch(function(error) {
const response = $.parseJSON(error.responseText);
if (response && response.errors) {
return(response.errors[0]);
} else {
return(I18n.t('generic_error'));
}
}, function (error) {
post.set('cooked', oldCooked);
self.set('composeState', OPEN);
const response = $.parseJSON(error.responseText);
throw response && response.errors ? response.errors[0] : I18n.t('generic_error');
});
});
},
serialize(serializer, dest) {
if (!dest) {
dest = {};
}
const self = this;
Object.keys(serializer).forEach(function(f) {
const val = self.get(serializer[f]);
dest = dest || {};
Object.keys(serializer).forEach(f => {
const val = this.get(serializer[f]);
if (typeof val !== 'undefined') {
Ember.set(dest, f, val);
}
@ -468,9 +466,10 @@ const Composer = Discourse.Model.extend({
// Create a new Post
createPost(opts) {
const post = this.get('post'),
topic = this.get('topic'),
currentUser = Discourse.User.current(),
postStream = this.get('topic.postStream');
topic = this.get('topic'),
currentUser = Discourse.User.current(),
postStream = this.get('topic.postStream');
let addedToStream = false;
// Build the post object
@ -530,10 +529,10 @@ const Composer = Discourse.Model.extend({
}
}
const composer = this;
const promise = new Ember.RSVP.Promise(function(resolve, reject) {
const composer = this,
promise = new Ember.RSVP.Promise(function(resolve, reject) {
composer.set('composeState', SAVING);
createdPost.save(function(result) {
let saving = true;

View File

@ -326,20 +326,17 @@ const PostStream = Ember.Object.extend({
// Commit the post we staged. Call this after a save succeeds.
commitPost(post) {
if (this.get('loadedAllPosts')) {
this.appendPost(post);
}
// Correct for a dangling deleted post, if needed
// compensating for message bus pumping in new posts while
// your post is in transit
if(this.get('topic.highest_post_number') < post.get('post_number')){
this.set('topic.highest_post_number', post.get('post_number'));
if (this.get('topic.id') === post.get('topic_id')) {
if (this.get('loadedAllPosts')) {
this.appendPost(post);
this.get('stream').addObject(post.get('id'));
}
}
this.get('stream').removeObject(-1);
this.get('postIdentityMap').set(-1, null);
this.get('postIdentityMap').set(post.get('id'), post);
this.get('stream').addObject(post.get('id'));
this.set('stagingPost', false);
},

View File

@ -0,0 +1,20 @@
export default Ember.Object.extend({
update(attrs) {
const self = this,
type = this.get('__type');
return this.store.update(type, this.get('id'), attrs).then(function(result) {
if (result && result[type]) {
Object.keys(result).forEach(function(k) {
attrs[k] = result[k];
});
}
self.setProperties(attrs);
return result;
});
},
destroyRecord() {
const type = this.get('__type');
return this.store.destroyRecord(type, this);
}
});

View File

@ -0,0 +1,22 @@
export default Ember.ArrayProxy.extend({
loading: false,
loadingMore: false,
totalRows: 0,
loadMore() {
const loadMoreUrl = this.get('loadMoreUrl');
if (!loadMoreUrl) { return; }
const totalRows = this.get('totalRows');
if (this.get('length') < totalRows && !this.get('loadingMore')) {
this.set('loadingMore', true);
const self = this;
return this.store.appendResults(this, this.get('__type'), loadMoreUrl).then(function() {
self.set('loadingMore', false);
});
}
return Ember.RSVP.resolve();
}
});

View File

@ -1,40 +1,49 @@
import RestModel from 'discourse/models/rest';
import ResultSet from 'discourse/models/result-set';
const _identityMap = {};
const RestModel = Ember.Object.extend({
update(attrs) {
const self = this,
type = this.get('__type');
return this.store.update(type, this.get('id'), attrs).then(function(result) {
if (result && result[type]) {
Object.keys(result).forEach(function(k) {
attrs[k] = result[k];
});
}
self.setProperties(attrs);
return result;
});
export default Ember.Object.extend({
pluralize(thing) {
return thing + "s";
},
destroyRecord() {
const type = this.get('__type');
return this.store.destroyRecord(type, this);
}
});
export default Ember.Object.extend({
findAll(type) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
const self = this;
return adapter.findAll(this, type).then(function(result) {
return result[Ember.String.underscore(type + 's')].map(obj => self._hydrate(type, obj));
return self._resultSet(type, result);
});
},
find(type, id) {
find(type, findArgs) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
const self = this;
return adapter.find(this, type, id).then(function(result) {
return self._hydrate(type, result[Ember.String.underscore(type)]);
return adapter.find(this, type, findArgs).then(function(result) {
if (typeof findArgs === "object") {
return self._resultSet(type, result);
} else {
return self._hydrate(type, result[Ember.String.underscore(type)]);
}
});
},
appendResults(resultSet, type, url) {
const self = this;
return Discourse.ajax(url).then(function(result) {
const typeName = Ember.String.underscore(self.pluralize(type)),
totalRows = result["total_rows_" + typeName] || result.get('totalRows'),
loadMoreUrl = result["load_more_" + typeName],
content = result[typeName].map(obj => self._hydrate(type, obj));
resultSet.setProperties({ totalRows, loadMoreUrl });
resultSet.get('content').pushObjects(content);
// If we've loaded them all, clear the load more URL
if (resultSet.get('length') >= totalRows) {
resultSet.set('loadMoreUrl', null);
}
});
},
@ -63,6 +72,15 @@ export default Ember.Object.extend({
});
},
_resultSet(type, result) {
const typeName = Ember.String.underscore(this.pluralize(type)),
content = result[typeName].map(obj => this._hydrate(type, obj)),
totalRows = result["total_rows_" + typeName] || content.length,
loadMoreUrl = result["load_more_" + typeName];
return ResultSet.create({ content, totalRows, loadMoreUrl, store: this, __type: type });
},
_hydrate(type, obj) {
if (!obj) { throw "Can't hydrate " + type + " of `null`"; }
if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; }

View File

@ -1,32 +0,0 @@
export default Ember.Object.extend({
title: null,
availablePeriods: function() {
var periods = this.get('periods');
if (!periods) { return; }
var self = this;
return periods.filter(function(p) {
return p !== self;
});
}.property('showMoreUrl'),
_createTitle: function() {
var id = this.get('id');
if (id) {
var title = "this_week";
if (id === "yearly") {
title = "this_year";
} else if (id === "monthly") {
title = "this_month";
} else if (id === "daily") {
title = "today";
} else if (id === "all") {
title = "all";
}
this.set('title', I18n.t("filters.top." + title));
}
}.on('init')
});

View File

@ -145,24 +145,16 @@ const Topic = Discourse.Model.extend({
toggleStatus(property) {
this.toggleProperty(property);
this.saveStatus(property, this.get(property) ? true : false);
},
setStatus(property, value) {
this.set(property, value);
this.saveStatus(property, value);
this.saveStatus(property, !!this.get(property));
},
saveStatus(property, value) {
if (property === 'closed' && value === true) {
this.set('details.auto_close_at', null);
}
if (property === 'pinned') {
this.set('pinned_at', value ? moment() : null);
}
return Discourse.ajax(this.get('url') + "/status", {
type: 'PUT',
data: {status: property, enabled: value ? 'true' : 'false' }
data: { status: property, enabled: !!value }
});
},
@ -187,63 +179,61 @@ const Topic = Discourse.Model.extend({
}.property('word_count'),
toggleBookmark() {
if (this.get("bookmarking")) { return; }
this.set("bookmarking", true);
const self = this,
stream = this.get('postStream'),
posts = Em.get(stream, 'posts'),
firstPost = posts &&
posts[0] &&
posts[0].get('post_number') === 1 &&
posts[0],
bookmark = !self.get('bookmarked');
firstPost = posts && posts[0] && posts[0].get('post_number') === 1 && posts[0],
bookmark = !this.get('bookmarked'),
path = bookmark ? '/bookmark' : '/remove_bookmarks';
var path = bookmark ? '/bookmark' : '/remove_bookmarks';
var unbookmarkedPosts = [],
bookmarkedPost;
const toggleBookmarkOnServer = function() {
return Discourse.ajax('/t/' + self.get('id') + path, {
type: 'PUT',
}).then(function() {
self.toggleProperty('bookmarked');
if (bookmark && firstPost) { firstPost.set('bookmarked', true); }
if (!bookmark && posts) {
posts.forEach((post) => post.get('bookmarked') && post.set('bookmarked', false));
}
}).catch(function(error) {
let showGenericError = true;
if (error && error.responseText) {
try {
bootbox.alert($.parseJSON(error.responseText).errors);
showGenericError = false;
} catch(e) { }
}
this.toggleProperty('bookmarked');
if (showGenericError) {
bootbox.alert(I18n.t('generic_error'));
}
if (bookmark && firstPost) {
firstPost.set('bookmarked', true);
bookmarkedPost = firstPost;
}
throw error;
}).finally(function() {
self.set("bookmarking", false);
});
};
let unbookmarkedPosts = [];
if (!bookmark && posts) {
posts.forEach(function(post){
if(post.get('bookmarked')){
post.set('bookmarked', false);
unbookmarkedPosts.push(post);
}
});
posts.forEach((post) => post.get('bookmarked') && unbookmarkedPosts.push(post));
}
return Discourse.ajax('/t/' + this.get('id') + path, {
type: 'PUT',
}).catch(function(error) {
self.toggleProperty('bookmarked');
if(bookmarkedPost) {
bookmarkedPost.set('bookmarked', false);
}
unbookmarkedPosts.forEach(function(p){
p.set('bookmarked', true);
});
let showGenericError = true;
if (error && error.responseText) {
try {
bootbox.alert($.parseJSON(error.responseText).errors);
showGenericError = false;
} catch(e){}
}
if(showGenericError){
bootbox.alert(I18n.t('generic_error'));
}
throw error;
});
if (unbookmarkedPosts.length > 1) {
return bootbox.confirm(
I18n.t("bookmarks.confirm_clear"),
I18n.t("no_value"),
I18n.t("yes_value"),
function (confirmed) {
if (confirmed) { return toggleBookmarkOnServer(); }
}
);
} else {
return toggleBookmarkOnServer();
}
},
createInvite(emailOrUsername, groupNames) {

View File

@ -189,7 +189,8 @@ const User = Discourse.Model.extend({
'enable_quoting',
'disable_jump_reply',
'custom_fields',
'user_fields');
'user_fields',
'muted_usernames');
['muted','watched','tracked'].forEach(function(s){
var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')});
@ -256,12 +257,7 @@ const User = Discourse.Model.extend({
ua.action_type === Discourse.UserAction.TYPES.topics;
},
/**
The user's stat count, excluding PMs.
@property statsCountNonPM
@type {Integer}
**/
// The user's stat count, excluding PMs.
statsCountNonPM: function() {
var self = this;
@ -275,12 +271,7 @@ const User = Discourse.Model.extend({
return count;
}.property('statsExcludingPms.@each.count'),
/**
The user's stats, excluding PMs.
@property statsExcludingPms
@type {Array}
**/
// The user's stats, excluding PMs.
statsExcludingPms: function() {
if (this.blank('stats')) return [];
return this.get('stats').rejectProperty('isPM');
@ -436,14 +427,9 @@ const User = Discourse.Model.extend({
User.reopenClass(Discourse.Singleton, {
/**
Find a `Discourse.User` for a given username.
@method findByUsername
@returns {Promise} a promise that resolves to a `Discourse.User`
**/
// Find a `Discourse.User` for a given username.
findByUsername: function(username, options) {
var user = Discourse.User.create({username: username});
const user = Discourse.User.create({username: username});
return user.findDetails(options);
},

View File

@ -52,6 +52,7 @@ export default function() {
});
// User routes
this.resource('users');
this.resource('user', { path: '/users/:username' }, function() {
this.resource('userActivity', { path: '/activity' }, function() {
var self = this;
@ -83,6 +84,7 @@ export default function() {
this.route('signup', {path: '/signup'});
this.route('login', {path: '/login'});
this.route('forgot-password', {path: '/password-reset'});
this.route('faq', {path: '/faq'});
this.route('tos', {path: '/tos'});
this.route('privacy', {path: '/privacy'});

View File

@ -102,7 +102,7 @@ const ApplicationRoute = Discourse.Route.extend({
// Close the current modal, and destroy its state.
closeModal() {
this.render('hide-modal', {into: 'modal', outlet: 'modalBody'});
this.render('hide-modal', { into: 'modal', outlet: 'modalBody' });
},
/**

View File

@ -67,14 +67,13 @@ export default function(filter, params) {
setupController: function(controller, model) {
var topics = this.get('topics'),
periods = this.controllerFor('discovery').get('periods'),
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
this.controllerFor('navigation/category').set('canCreateTopic', topics.get('can_create_topic'));
this.controllerFor('discovery/topics').setProperties({
model: topics,
category: model,
period: periods.findBy('id', periodId),
period: periodId,
selected: [],
noSubcategories: params && !!params.no_subcategories,
order: topics.get('params.order'),

View File

@ -45,13 +45,11 @@ export default function(filter, extras) {
})));
}
const periods = this.controllerFor('discovery').get('periods'),
periodId = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
const period = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
const topicOpts = {
model,
category: null,
period: periods.findBy('id', periodId),
period,
selected: [],
expandGloballyPinned: true
};

View File

@ -2,6 +2,23 @@ import showModal from 'discourse/lib/show-modal';
const DiscourseRoute = Ember.Route.extend({
// Set to true to refresh a model without a transition if a query param
// changes
resfreshQueryWithoutTransition: false,
refresh: function() {
if (!this.refreshQueryWithoutTransition) { return this._super(); }
if (!this.router.router.activeTransition) {
const controller = this.controller,
model = controller.get('model'),
params = this.controller.getProperties(Object.keys(this.queryParams));
model.set('loading', true);
this.model(params).then(model => this.setupController(controller, model));
}
},
/**
NOT called every time we enter a route on Discourse.
Only called the FIRST time we enter a route.

View File

@ -0,0 +1,22 @@
export default Discourse.Route.extend({
beforeModel: function() {
this.replaceWith('discovery.latest').then(function(e) {
Ember.run.next(function() {
e.send('showForgotPassword');
});
});
},
model: function() {
return Discourse.StaticPage.find('password-reset');
},
renderTemplate: function() {
// do nothing
this.render('static');
},
setupController: function(controller, model) {
this.controllerFor('static').set('model', model);
}
});

View File

@ -8,7 +8,10 @@ export default RestrictedUserRoute.extend(ShowFooter, {
},
setupController(controller, user) {
controller.setProperties({ model: user, newNameInput: user.get('name') });
controller.setProperties({
model: user,
newNameInput: user.get('name')
});
},
actions: {
@ -16,15 +19,15 @@ export default RestrictedUserRoute.extend(ShowFooter, {
showModal('avatar-selector');
// all the properties needed for displaying the avatar selector modal
const controller = this.controllerFor('avatar-selector');
const user = this.modelFor('user');
const props = user.getProperties(
'username', 'email',
'uploaded_avatar_id',
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
);
const controller = this.controllerFor('avatar-selector'),
props = this.modelFor('user').getProperties(
'email',
'username',
'uploaded_avatar_id',
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
);
switch (props.uploaded_avatar_id) {
case props.system_avatar_upload_id:
@ -40,20 +43,20 @@ export default RestrictedUserRoute.extend(ShowFooter, {
controller.setProperties(props);
},
saveAvatarSelection: function() {
const user = this.modelFor('user');
const avatarSelector = this.controllerFor('avatar-selector');
saveAvatarSelection() {
const user = this.modelFor('user'),
avatarSelector = this.controllerFor('avatar-selector');
// sends the information to the server if it has changed
if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) {
user.pickAvatar(avatarSelector.get('selectedUploadId'))
.then(function(){
user.setProperties(avatarSelector.getProperties(
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
));
});
.then(() => {
user.setProperties(avatarSelector.getProperties(
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
));
});
}
// saves the data back

View File

@ -2,9 +2,8 @@
export default Discourse.Route.extend({
afterModel: function() {
var user = this.modelFor('user');
if (!user.get('can_edit')) {
afterModel() {
if (!this.modelFor('user').get('can_edit')) {
this.replaceWith('userActivity');
}
}

View File

@ -59,6 +59,11 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, {
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
},
showFeatureTopic() {
showModal('featureTopic', this.modelFor('topic'));
this.controllerFor('modal').set('modalClass', 'feature-topic-modal');
},
showInvite() {
showModal('invite', this.modelFor('topic'));
this.controllerFor('invite').reset();

View File

@ -0,0 +1,36 @@
export default Discourse.Route.extend({
queryParams: {
period: { refreshModel: true },
order: { refreshModel: true },
asc: { refreshModel: true },
name: { refreshModel: true, replace: true }
},
refreshQueryWithoutTransition: true,
titleToken() {
return I18n.t('directory.title');
},
resetController(controller, isExiting) {
if (isExiting) {
controller.setProperties({
period: 'weekly',
order: 'likes_received',
asc: null,
name: ''
});
}
},
model(params) {
// If we refresh via `refreshModel` set the old model to loading
this._params = params;
return this.store.find('directoryItem', params);
},
setupController(controller, model) {
const params = this._params;
controller.setProperties({ model, period: params.period, nameInput: params.name });
}
});

View File

@ -0,0 +1,11 @@
<h2>{{period-title period}}</h2>
<button>{{fa-icon "caret-down"}}</button>
<div id='period-popup' {{bind-attr class="showPeriods::hidden :period-popup"}}>
<ul>
{{#each p in site.periods}}
<li><a href {{action "changePeriod" p}}>{{period-title p}}</a></li>
{{/each}}
</ul>
</div>
<div class='clearfix'></div>

View File

@ -1 +1,7 @@
<a href {{action "share" source}} {{bind-attr title="title"}}><i {{bind-attr class=":fa source.iconClass"}}></i></a>
<a href {{action "share" source}} {{bind-attr title="title"}}>
{{#if source.faIcon}}
<i {{bind-attr class=":fa source.faIcon"}}></i>
{{else}}
{{{source.htmlIcon}}}
{{/if}}
</a>

View File

@ -1,3 +1,5 @@
{{#each p in period.availablePeriods}}
{{top-title-button period=p}}
{{#each p in periods}}
{{#d-button action="changePeriod" actionParam=p}}
{{period-title p}}
{{/d-button}}
{{/each}}

View File

@ -1,11 +0,0 @@
{{top-title period=period}}
<button><i class='fa fa-caret-down'></i></button>
<div id='period-popup' {{bind-attr class="showPeriods::hidden"}}>
<ul>
{{#each p in period.availablePeriods}}
<li><a {{bind-attr href="p.showMoreUrl"}}>{{top-title tagName="span" period=p}}</a></li>
{{/each}}
</ul>
</div>
<div class='clearfix'></div>

View File

@ -1,9 +1,11 @@
<div class="user-image">
{{#link-to 'user' user.username}}{{avatar user imageSize="large"}}{{/link-to}}
<a href="{{unbound userPath}}" data-user-card="{{unbound user.username}}">{{avatar user imageSize="large"}}</a>
</div>
<div class="user-detail">
<span class="username">{{#link-to 'user' user.username}}{{user.username}}{{/link-to}}</span>
<span class="name">{{user.name}}</span>
<span class="title">{{user.title}}</span>
<div class='name-line'>
<span class="username"><a href="{{unbound userPath}}" data-user-card="{{unbound user.username}}">{{unbound user.username}}</a></span>
<span class="name">{{unbound name}}</span>
</div>
<div class="title">{{unbound user.title}}</div>
</div>

View File

@ -95,7 +95,9 @@ so I'm going to stop rendering it until we figure out what's up
<a {{bind-attr class=":mobile-file-upload view.isUploading:hidden"}}>{{i18n 'upload'}}</a>
<input type="file" id="mobile-uploader" />
{{/if}}
<div id='draft-status'>{{model.draftStatus}}</div>
<div id='draft-status' {{bind-attr class="view.isUploading:hidden"}}>
{{model.draftStatus}}
</div>
</div>
</div>

View File

@ -19,7 +19,7 @@
<div class='contents'>
{{#if top}}
<div class='top-lists'>
{{top-period-chooser period=period}}
{{period-chooser period=period action="changePeriod"}}
</div>
{{/if}}
{{#if topicTrackingState.hasIncoming}}
@ -73,7 +73,7 @@
{{#if top}}
<h3>
{{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}}
{{top-period-buttons period=period}}
{{top-period-buttons period=period action="changePeriod"}}
</h3>
{{else}}
<div class="education">

View File

@ -1,7 +1,7 @@
<div class='contents'>
{{#if top}}
<div class='top-lists'>
{{top-period-chooser period=period}}
{{period-chooser period=period action="changePeriod"}}
</div>
{{/if}}
@ -45,7 +45,7 @@
<h3>
{{#link-to "discovery.categories"}}{{i18n 'topic.browse_all_categories'}}{{/link-to}}, {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}} {{i18n 'or'}} {{i18n 'filters.top.other_periods'}}
<br/>
{{top-period-buttons period=period}}
{{top-period-buttons period=period action="changePeriod"}}
</h3>
{{else}}
<div class="education">

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