Version bump
This commit is contained in:
commit
c7040e46b9
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 () {
|
||||
@ -1,5 +1,4 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
import ObjectController from 'discourse/controllers/object';
|
||||
|
||||
export default ObjectController.extend(ModalFunctionality, {
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
@ -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');
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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'>
|
||||
—
|
||||
—
|
||||
</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>
|
||||
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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')
|
||||
});
|
||||
@ -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' });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -32,5 +32,6 @@ export default Ember.Component.extend({
|
||||
|
||||
click() {
|
||||
this.sendAction("action", this.get("actionParam"));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@ -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>';
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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'));
|
||||
}
|
||||
});
|
||||
@ -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')
|
||||
|
||||
});
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default Ember.Controller.extend({
|
||||
me: Discourse.computed.propertyEqual('model.user.id', 'currentUser.id')
|
||||
});
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -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"); },
|
||||
}
|
||||
|
||||
});
|
||||
@ -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'),
|
||||
|
||||
@ -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"});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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); },
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
|
||||
19
app/assets/javascripts/discourse/controllers/users.js.es6
Normal file
19
app/assets/javascripts/discourse/controllers/users.js.es6
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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"));
|
||||
|
||||
@ -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];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
11
app/assets/javascripts/discourse/helpers/period-title.js.es6
Normal file
11
app/assets/javascripts/discourse/helpers/period-title.js.es6
Normal 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);
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
61
app/assets/javascripts/discourse/lib/safari-hacks.js.es6
Normal file
61
app/assets/javascripts/discourse/lib/safari-hacks.js.es6
Normal 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;
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
|
||||
20
app/assets/javascripts/discourse/models/rest.js.es6
Normal file
20
app/assets/javascripts/discourse/models/rest.js.es6
Normal 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);
|
||||
}
|
||||
});
|
||||
22
app/assets/javascripts/discourse/models/result-set.js.es6
Normal file
22
app/assets/javascripts/discourse/models/result-set.js.es6
Normal 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();
|
||||
}
|
||||
});
|
||||
@ -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`"; }
|
||||
|
||||
@ -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')
|
||||
|
||||
});
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
|
||||
@ -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'});
|
||||
|
||||
@ -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' });
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
36
app/assets/javascripts/discourse/routes/users.js.es6
Normal file
36
app/assets/javascripts/discourse/routes/users.js.es6
Normal 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 });
|
||||
}
|
||||
});
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
Reference in New Issue
Block a user