mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-12-02 11:59:17 -05:00
FIX: Many bugs with admin badges interface
* Editing a badge's title would show it as changed in the side even if you didn't hit save * Clicking a badge would not scroll to the top * If there was an error saving a badge there was a missing i18n key * URLs were using queryParams instead of paths * User `label` tags for checkboxes for larger click targets * Saved! text would persist when viewing another badge * After creating a new badge it would show nothing * Validation errors were not being properly released to the client * Query errors were surrounded by an extra array
This commit is contained in:
parent
ab9a0235b4
commit
0cbdf6f5bb
17 changed files with 359 additions and 495 deletions
|
@ -1,37 +0,0 @@
|
||||||
import ObjectController from 'discourse/controllers/object';
|
|
||||||
|
|
||||||
/**
|
|
||||||
This is the itemController for `Discourse.AdminBadgesController`. Its main purpose
|
|
||||||
is to indicate which badge was selected.
|
|
||||||
|
|
||||||
@class AdminBadgeController
|
|
||||||
@extends ObjectController
|
|
||||||
@namespace Discourse
|
|
||||||
@module Discourse
|
|
||||||
**/
|
|
||||||
|
|
||||||
export default ObjectController.extend({
|
|
||||||
/**
|
|
||||||
Whether this badge has been selected.
|
|
||||||
|
|
||||||
@property selected
|
|
||||||
@type {Boolean}
|
|
||||||
**/
|
|
||||||
selected: Discourse.computed.propertyEqual('model.name', 'parentController.selectedItem.name'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
Show the displayName only if it is different from the name.
|
|
||||||
|
|
||||||
@property showDisplayName
|
|
||||||
@type {Boolean}
|
|
||||||
**/
|
|
||||||
showDisplayName: Discourse.computed.propertyNotEqual('selectedItem.name', 'selectedItem.displayName'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
Don't allow editing if this is a system badge.
|
|
||||||
|
|
||||||
@property readOnly
|
|
||||||
@type {Boolean}
|
|
||||||
**/
|
|
||||||
readOnly: Ember.computed.alias('model.system')
|
|
||||||
});
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||||
|
|
||||||
|
export default Ember.ObjectController.extend(BufferedContent, {
|
||||||
|
needs: ['admin-badges'],
|
||||||
|
saving: false,
|
||||||
|
savingStatus: '',
|
||||||
|
|
||||||
|
badgeTypes: Em.computed.alias('controllers.admin-badges.badgeTypes'),
|
||||||
|
badgeGroupings: Em.computed.alias('controllers.admin-badges.badgeGroupings'),
|
||||||
|
badgeTriggers: Em.computed.alias('controllers.admin-badges.badgeTriggers'),
|
||||||
|
protectedSystemFields: Em.computed.alias('controllers.admin-badges.protectedSystemFields'),
|
||||||
|
|
||||||
|
readOnly: Ember.computed.alias('buffered.system'),
|
||||||
|
showDisplayName: Discourse.computed.propertyNotEqual('name', 'displayName'),
|
||||||
|
canEditDescription: Em.computed.none('buffered.translatedDescription'),
|
||||||
|
|
||||||
|
_resetSaving: function() {
|
||||||
|
this.set('saving', false);
|
||||||
|
this.set('savingStatus', '');
|
||||||
|
}.observes('model.id'),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
save: function() {
|
||||||
|
if (!this.get('saving')) {
|
||||||
|
var fields = ['allow_title', 'multiple_grant',
|
||||||
|
'listable', 'auto_revoke',
|
||||||
|
'enabled', 'show_posts',
|
||||||
|
'target_posts', 'name', 'description',
|
||||||
|
'icon', 'query', 'badge_grouping_id',
|
||||||
|
'trigger', 'badge_type_id'],
|
||||||
|
self = this;
|
||||||
|
|
||||||
|
if (this.get('buffered.system')){
|
||||||
|
var protectedFields = this.get('protectedSystemFields');
|
||||||
|
fields = _.filter(fields, function(f){
|
||||||
|
return !_.include(protectedFields,f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('saving', true);
|
||||||
|
this.set('savingStatus', I18n.t('saving'));
|
||||||
|
|
||||||
|
var boolFields = ['allow_title', 'multiple_grant',
|
||||||
|
'listable', 'auto_revoke',
|
||||||
|
'enabled', 'show_posts',
|
||||||
|
'target_posts' ];
|
||||||
|
|
||||||
|
var data = {},
|
||||||
|
buffered = this.get('buffered');
|
||||||
|
fields.forEach(function(field){
|
||||||
|
var d = buffered.get(field);
|
||||||
|
if (_.include(boolFields, field)) { d = !!d; }
|
||||||
|
data[field] = d;
|
||||||
|
});
|
||||||
|
|
||||||
|
var newBadge = !this.get('id'),
|
||||||
|
model = this.get('model');
|
||||||
|
this.get('model').save(data).then(function() {
|
||||||
|
if (newBadge) {
|
||||||
|
self.get('controllers.admin-badges').pushObject(model);
|
||||||
|
self.transitionToRoute('adminBadges.show', model.get('id'));
|
||||||
|
} else {
|
||||||
|
self.commitBuffer();
|
||||||
|
self.set('savingStatus', I18n.t('saved'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}).catch(function(error) {
|
||||||
|
self.set('savingStatus', I18n.t('failed'));
|
||||||
|
self.send('saveError', error);
|
||||||
|
}).finally(function() {
|
||||||
|
self.set('saving', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function() {
|
||||||
|
var self = this,
|
||||||
|
adminBadgesController = this.get('controllers.admin-badges'),
|
||||||
|
model = this.get('model');
|
||||||
|
|
||||||
|
if (!model.get('id')) {
|
||||||
|
self.transitionToRoute('adminBadges.index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bootbox.confirm(I18n.t("admin.badges.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||||
|
if (result) {
|
||||||
|
model.destroy().then(function() {
|
||||||
|
adminBadgesController.removeObject(model);
|
||||||
|
self.transitionToRoute('adminBadges.index');
|
||||||
|
}).catch(function() {
|
||||||
|
bootbox.alert(I18n.t('generic_error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,165 +1 @@
|
||||||
/**
|
export default Ember.ArrayController.extend();
|
||||||
This controller supports the interface for dealing with badges.
|
|
||||||
|
|
||||||
@class AdminBadgesController
|
|
||||||
@extends Ember.ArrayController
|
|
||||||
@namespace Discourse
|
|
||||||
@module Discourse
|
|
||||||
**/
|
|
||||||
export default Ember.ArrayController.extend({
|
|
||||||
needs: ['modal'],
|
|
||||||
itemController: 'admin-badge',
|
|
||||||
queryParams: ['badgeId'],
|
|
||||||
badgeId: Em.computed.alias('selectedId'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
ID of the currently selected badge.
|
|
||||||
|
|
||||||
@property selectedId
|
|
||||||
@type {Integer}
|
|
||||||
**/
|
|
||||||
selectedId: null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
Badge that is currently selected.
|
|
||||||
|
|
||||||
@property selectedItem
|
|
||||||
@type {Discourse.Badge}
|
|
||||||
**/
|
|
||||||
selectedItem: function() {
|
|
||||||
if (this.get('selectedId') === undefined || this.get('selectedId') === "undefined") {
|
|
||||||
// New Badge
|
|
||||||
return this.get('newBadge');
|
|
||||||
} else {
|
|
||||||
// Existing Badge
|
|
||||||
var selectedId = parseInt(this.get('selectedId'));
|
|
||||||
return this.get('model').filter(function(badge) {
|
|
||||||
return parseInt(badge.get('id')) === selectedId;
|
|
||||||
})[0];
|
|
||||||
}
|
|
||||||
}.property('selectedId', 'newBadge'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
Unsaved badge, if one exists.
|
|
||||||
|
|
||||||
@property newBadge
|
|
||||||
@type {Discourse.Badge}
|
|
||||||
**/
|
|
||||||
newBadge: function() {
|
|
||||||
return this.get('model').filter(function(badge) {
|
|
||||||
return badge.get('id') === undefined;
|
|
||||||
})[0];
|
|
||||||
}.property('model.@each.id'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
Whether a new unsaved badge exists.
|
|
||||||
|
|
||||||
@property newBadgeExists
|
|
||||||
@type {Discourse.Badge}
|
|
||||||
**/
|
|
||||||
newBadgeExists: Em.computed.notEmpty('newBadge'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
We don't allow setting a description if a translation for the given badge
|
|
||||||
name exists.
|
|
||||||
|
|
||||||
@property canEditDescription
|
|
||||||
@type {Boolean}
|
|
||||||
**/
|
|
||||||
canEditDescription: Em.computed.none('selectedItem.translatedDescription'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
Disable saving if the currently selected item is being saved.
|
|
||||||
|
|
||||||
@property disableSave
|
|
||||||
@type {Boolean}
|
|
||||||
**/
|
|
||||||
disableSave: Em.computed.alias('selectedItem.saving'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
/**
|
|
||||||
Create a new badge and select it.
|
|
||||||
|
|
||||||
@method newBadge
|
|
||||||
**/
|
|
||||||
createNewBadge: function() {
|
|
||||||
var badge = Discourse.Badge.create({
|
|
||||||
name: I18n.t('admin.badges.new_badge')
|
|
||||||
});
|
|
||||||
this.pushObject(badge);
|
|
||||||
this.send('selectBadge', badge);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Select a particular badge.
|
|
||||||
|
|
||||||
@method selectBadge
|
|
||||||
@param {Discourse.Badge} badge The badge to be selected
|
|
||||||
**/
|
|
||||||
selectBadge: function(badge) {
|
|
||||||
this.set('selectedId', badge.get('id'));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Save the selected badge.
|
|
||||||
|
|
||||||
@method save
|
|
||||||
**/
|
|
||||||
save: function() {
|
|
||||||
if (!this.get('disableSave')) {
|
|
||||||
var fields = ['allow_title', 'multiple_grant',
|
|
||||||
'listable', 'auto_revoke',
|
|
||||||
'enabled', 'show_posts',
|
|
||||||
'target_posts', 'name', 'description',
|
|
||||||
'icon', 'query', 'badge_grouping_id',
|
|
||||||
'trigger', 'badge_type_id'],
|
|
||||||
self = this;
|
|
||||||
|
|
||||||
if (this.get('selectedItem.system')){
|
|
||||||
var protectedFields = this.get('protectedSystemFields');
|
|
||||||
fields = _.filter(fields, function(f){
|
|
||||||
return !_.include(protectedFields,f);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.get('selectedItem').save(fields).catch(function(error) {
|
|
||||||
// this shows the admin-badge-preview modal with the error
|
|
||||||
// kinda weird, but it consolidates the display logic for badge errors
|
|
||||||
self.send('saveError', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Confirm before destroying the selected badge.
|
|
||||||
|
|
||||||
@method destroy
|
|
||||||
**/
|
|
||||||
destroy: function() {
|
|
||||||
// Delete immediately if the selected badge is new.
|
|
||||||
if (!this.get('selectedItem.id')) {
|
|
||||||
this.get('model').removeObject(this.get('selectedItem'));
|
|
||||||
this.set('selectedId', null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
return bootbox.confirm(I18n.t("admin.badges.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
|
||||||
if (result) {
|
|
||||||
var selected = self.get('selectedItem');
|
|
||||||
selected.destroy().then(function() {
|
|
||||||
// Success.
|
|
||||||
self.set('selectedId', null);
|
|
||||||
self.get('model').removeObject(selected);
|
|
||||||
}, function() {
|
|
||||||
// Failure.
|
|
||||||
bootbox.alert(I18n.t('generic_error'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
52
app/assets/javascripts/admin/routes/admin-badges-show.js.es6
Normal file
52
app/assets/javascripts/admin/routes/admin-badges-show.js.es6
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
export default Ember.Route.extend({
|
||||||
|
serialize: function(m) {
|
||||||
|
return {badge_id: Em.get(m, 'id') || 'new'};
|
||||||
|
},
|
||||||
|
|
||||||
|
model: function(params) {
|
||||||
|
if (params.badge_id === "new") {
|
||||||
|
return Discourse.Badge.create({
|
||||||
|
name: I18n.t('admin.badges.new_badge')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.modelFor('adminBadges').findProperty('id', parseInt(params.badge_id));
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
saveError: function(e) {
|
||||||
|
var msg = I18n.t("generic_error");
|
||||||
|
if (e.responseJSON && e.responseJSON.errors) {
|
||||||
|
msg = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')});
|
||||||
|
}
|
||||||
|
bootbox.alert(msg);
|
||||||
|
},
|
||||||
|
|
||||||
|
editGroupings: function() {
|
||||||
|
var groupings = this.controllerFor('admin-badges').get('badgeGroupings');
|
||||||
|
Discourse.Route.showModal(this, 'admin_edit_badge_groupings', groupings);
|
||||||
|
},
|
||||||
|
|
||||||
|
preview: function(badge, explain) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
badge.set('preview_loading', true);
|
||||||
|
Discourse.ajax('/admin/badges/preview.json', {
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
sql: badge.get('query'),
|
||||||
|
target_posts: !!badge.get('target_posts'),
|
||||||
|
trigger: badge.get('trigger'),
|
||||||
|
explain: explain
|
||||||
|
}
|
||||||
|
}).then(function(json) {
|
||||||
|
badge.set('preview_loading', false);
|
||||||
|
Discourse.Route.showModal(self, 'admin_badge_preview', json);
|
||||||
|
}).catch(function(error) {
|
||||||
|
badge.set('preview_loading', false);
|
||||||
|
Em.Logger.error(error);
|
||||||
|
bootbox.alert("Network error");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
28
app/assets/javascripts/admin/routes/admin-badges.js.es6
Normal file
28
app/assets/javascripts/admin/routes/admin-badges.js.es6
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
_json: null,
|
||||||
|
|
||||||
|
model: function() {
|
||||||
|
var self = this;
|
||||||
|
return Discourse.ajax('/admin/badges.json').then(function(json) {
|
||||||
|
self._json = json;
|
||||||
|
return Discourse.Badge.createFromJson(json);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController: function(controller, model) {
|
||||||
|
var json = this._json,
|
||||||
|
triggers = [];
|
||||||
|
|
||||||
|
_.each(json.admin_badges.triggers,function(v,k){
|
||||||
|
triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)});
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.setProperties({
|
||||||
|
badgeGroupings: json.badge_groupings,
|
||||||
|
badgeTypes: json.badge_types,
|
||||||
|
protectedSystemFields: json.admin_badges.protected_system_fields,
|
||||||
|
badgeTriggers: triggers,
|
||||||
|
model: model
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,54 +0,0 @@
|
||||||
Discourse.AdminBadgesRoute = Discourse.Route.extend({
|
|
||||||
setupController: function(controller) {
|
|
||||||
Discourse.ajax('/admin/badges.json').then(function(json){
|
|
||||||
|
|
||||||
controller.set('badgeGroupings', Em.A(json.badge_groupings));
|
|
||||||
controller.set('badgeTypes', json.badge_types);
|
|
||||||
controller.set('protectedSystemFields', json.admin_badges.protected_system_fields);
|
|
||||||
var triggers = [];
|
|
||||||
_.each(json.admin_badges.triggers,function(v,k){
|
|
||||||
triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)});
|
|
||||||
});
|
|
||||||
controller.set('badgeTriggers', triggers);
|
|
||||||
controller.set('model', Discourse.Badge.createFromJson(json));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
editGroupings: function(model) {
|
|
||||||
Discourse.Route.showModal(this, 'admin_edit_badge_groupings', model);
|
|
||||||
},
|
|
||||||
|
|
||||||
saveError: function(jqXhr) {
|
|
||||||
if (jqXhr.status === 422) {
|
|
||||||
Discourse.Route.showModal(this, 'admin_badge_preview', jqXhr.responseJSON);
|
|
||||||
} else {
|
|
||||||
Em.Logger.error(jqXhr);
|
|
||||||
bootbox.alert(I18n.t('errors.description.unknown'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
preview: function(badge, explain) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
badge.set('preview_loading', true);
|
|
||||||
Discourse.ajax('/admin/badges/preview.json', {
|
|
||||||
method: 'post',
|
|
||||||
data: {
|
|
||||||
sql: badge.query,
|
|
||||||
target_posts: !!badge.target_posts,
|
|
||||||
trigger: badge.trigger,
|
|
||||||
explain: explain
|
|
||||||
}
|
|
||||||
}).then(function(json) {
|
|
||||||
badge.set('preview_loading', false);
|
|
||||||
Discourse.Route.showModal(self, 'admin_badge_preview', json);
|
|
||||||
}).catch(function(error) {
|
|
||||||
badge.set('preview_loading', false);
|
|
||||||
Em.Logger.error(error);
|
|
||||||
bootbox.alert("Network error");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
|
@ -58,7 +58,9 @@ Discourse.Route.buildRoutes(function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('badges');
|
this.resource('adminBadges', { path: '/badges' }, function() {
|
||||||
|
this.route('show', { path: '/:badge_id' });
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<li>{{#link-to 'adminUsersList'}}{{i18n admin.users.title}}{{/link-to}}</li>
|
<li>{{#link-to 'adminUsersList'}}{{i18n admin.users.title}}{{/link-to}}</li>
|
||||||
{{#if showBadges}}
|
{{#if showBadges}}
|
||||||
<li>{{#link-to 'admin.badges'}}{{i18n admin.badges.title}}{{/link-to}}</li>
|
<li>{{#link-to 'adminBadges.index'}}{{i18n admin.badges.title}}{{/link-to}}</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if currentUser.admin}}
|
{{#if currentUser.admin}}
|
||||||
<li>{{#link-to 'adminGroups.index'}}{{i18n admin.groups.title}}{{/link-to}}</li>
|
<li>{{#link-to 'adminGroups.index'}}{{i18n admin.groups.title}}{{/link-to}}</li>
|
||||||
|
|
9
app/assets/javascripts/admin/templates/badges-index.hbs
Normal file
9
app/assets/javascripts/admin/templates/badges-index.hbs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<div class='span13'>
|
||||||
|
<p>{{i18n admin.badges.none_selected}}</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{#link-to 'adminBadges.show' 'new' class="btn"}}
|
||||||
|
{{fa-icon "plus"}} {{i18n admin.badges.new}}
|
||||||
|
{{/link-to}}
|
||||||
|
</div>
|
||||||
|
</div>
|
140
app/assets/javascripts/admin/templates/badges-show.hbs
Normal file
140
app/assets/javascripts/admin/templates/badges-show.hbs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
<div class='current-badge span13'>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div>
|
||||||
|
<label for="name">{{i18n admin.badges.name}}</label>
|
||||||
|
{{input type="text" name="name" value=buffered.name}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if showDisplayName}}
|
||||||
|
<div>
|
||||||
|
<strong>{{i18n admin.badges.display_name}}</strong>
|
||||||
|
{{buffered.displayName}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="name">{{i18n admin.badges.icon}}</label>
|
||||||
|
{{input type="text" name="name" value=buffered.icon}}
|
||||||
|
<p class='help'>{{i18n admin.badges.icon_help}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="badge_type_id">{{i18n admin.badges.badge_type}}</label>
|
||||||
|
{{view Ember.Select name="badge_type_id"
|
||||||
|
value=buffered.badge_type_id
|
||||||
|
content=badgeTypes
|
||||||
|
optionValuePath="content.id"
|
||||||
|
optionLabelPath="content.name"
|
||||||
|
disabled=readOnly}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="badge_grouping_id">{{i18n admin.badges.badge_grouping}}</label>
|
||||||
|
{{view Ember.Select name="badge_grouping_id"
|
||||||
|
value=buffered.badge_grouping_id
|
||||||
|
content=badgeGroupings
|
||||||
|
optionValuePath="content.id"
|
||||||
|
optionLabelPath="content.name"}}
|
||||||
|
<button {{action "editGroupings"}} class='btn'>{{fa-icon 'pencil'}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="description">{{i18n admin.badges.description}}</label>
|
||||||
|
{{#if canEditDescription}}
|
||||||
|
{{textarea name="description" value=buffered.description}}
|
||||||
|
{{else}}
|
||||||
|
{{textarea name="description" value=buffered.displayDescription disabled=true}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="query">{{i18n admin.badges.query}}</label>
|
||||||
|
{{textarea name="query" value=buffered.query disabled=readOnly}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if hasQuery}}
|
||||||
|
<a href {{action "preview" buffered "false"}}>{{i18n admin.badges.preview.link_text}}</a>
|
||||||
|
|
|
||||||
|
<a href {{action "preview" buffered "true"}}>{{i18n admin.badges.preview.plan_text}}</a>
|
||||||
|
{{#if preview_loading}}
|
||||||
|
{{i18n loading}}...
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=buffered.auto_revoke disabled=readOnly}}
|
||||||
|
{{i18n admin.badges.auto_revoke}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=buffered.target_posts disabled=readOnly}}
|
||||||
|
{{i18n admin.badges.target_posts}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="trigger">{{i18n admin.badges.trigger}}</label>
|
||||||
|
{{view Ember.Select name="trigger"
|
||||||
|
value=buffered.trigger
|
||||||
|
content=badgeTriggers
|
||||||
|
optionValuePath="content.id"
|
||||||
|
optionLabelPath="content.name"
|
||||||
|
disabled=readOnly}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=buffered.allow_title}}
|
||||||
|
{{i18n admin.badges.allow_title}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=buffered.multiple_grant disabled=readOnly}}
|
||||||
|
{{i18n admin.badges.multiple_grant}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=buffered.listable disabled=readOnly}}
|
||||||
|
{{i18n admin.badges.listable}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=buffered.show_posts disabled=readOnly}}
|
||||||
|
{{i18n admin.badges.show_posts}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" checked=buffered.enabled}}
|
||||||
|
{{i18n admin.badges.enabled}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='buttons'>
|
||||||
|
<button {{action "save"}} {{bind-attr disabled=saving}} class='btn btn-primary'>{{i18n admin.badges.save}}</button>
|
||||||
|
<span class='saving'>{{savingStatus}}</span>
|
||||||
|
{{#unless readOnly}}
|
||||||
|
<a {{action "destroy"}} class='delete-link'>{{i18n admin.badges.delete}}</a>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if grant_count}}
|
||||||
|
<div class="span13 current-badge-actions">
|
||||||
|
<div>
|
||||||
|
{{#link-to 'badges.show' this}}{{i18n badges.granted count=grant_count}}{{/link-to}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
|
@ -5,160 +5,21 @@
|
||||||
<ul>
|
<ul>
|
||||||
{{#each}}
|
{{#each}}
|
||||||
<li>
|
<li>
|
||||||
<a {{action "selectBadge" this}} {{bind-attr class="selected:active"}}>
|
{{#link-to 'adminBadges.show' id}}
|
||||||
{{badge-button badge=this}}
|
{{badge-button badge=this}}
|
||||||
{{#if newBadge}}
|
{{#if newBadge}}
|
||||||
<span class="list-badge">{{i18n filters.new.lower_title}}</span>
|
<span class="list-badge">{{i18n filters.new.lower_title}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</a>
|
{{/link-to}}
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
<button {{action "createNewBadge"}} {{bind-attr disabled=newBadgeExists}} class='btn'><i class="fa fa-plus"></i>{{i18n admin.badges.new}}</button>
|
{{#link-to 'adminBadges.show' 'new' class="btn"}}
|
||||||
|
{{fa-icon "plus"}} {{i18n admin.badges.new}}
|
||||||
|
{{/link-to}}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if selectedItem}}
|
{{outlet}}
|
||||||
{{#with selectedItem controller='adminBadge'}}
|
|
||||||
<div class='current-badge span13'>
|
|
||||||
<form class="form-horizontal">
|
|
||||||
<div>
|
|
||||||
<label for="name">{{i18n admin.badges.name}}</label>
|
|
||||||
{{input type="text" name="name" value=name}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if showDisplayName}}
|
|
||||||
<div>
|
|
||||||
<strong>{{i18n admin.badges.display_name}}</strong>
|
|
||||||
{{displayName}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="name">{{i18n admin.badges.icon}}</label>
|
|
||||||
{{input type="text" name="name" value=icon}}
|
|
||||||
<p class='help'>{{i18n admin.badges.icon_help}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="badge_type_id">{{i18n admin.badges.badge_type}}</label>
|
|
||||||
{{view Ember.Select name="badge_type_id" value=badge_type_id
|
|
||||||
content=controller.badgeTypes
|
|
||||||
optionValuePath="content.id"
|
|
||||||
optionLabelPath="content.name"
|
|
||||||
disabled=readOnly}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="badge_grouping_id">{{i18n admin.badges.badge_grouping}}</label>
|
|
||||||
{{view Ember.Select name="badge_grouping_id" value=badge_grouping_id
|
|
||||||
content=controller.badgeGroupings
|
|
||||||
optionValuePath="content.id"
|
|
||||||
optionLabelPath="content.name"}}
|
|
||||||
<button {{action "editGroupings" controller.badgeGroupings}}><i class="fa fa-pencil"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="description">{{i18n admin.badges.description}}</label>
|
|
||||||
{{#if controller.canEditDescription}}
|
|
||||||
{{textarea name="description" value=description}}
|
|
||||||
{{else}}
|
|
||||||
{{textarea name="description" value=displayDescription disabled=true}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="query">{{i18n admin.badges.query}}</label>
|
|
||||||
{{textarea name="query" value=query disabled=readOnly}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if hasQuery}}
|
|
||||||
|
|
||||||
<a href {{action "preview" this "false"}}>{{i18n admin.badges.preview.link_text}}</a>
|
|
||||||
|
|
|
||||||
<a href {{action "preview" this "true"}}>{{i18n admin.badges.preview.plan_text}}</a>
|
|
||||||
{{#if preview_loading}}
|
|
||||||
{{i18n loading}}...
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{{input type="checkbox" checked=auto_revoke disabled=readOnly}}
|
|
||||||
{{i18n admin.badges.auto_revoke}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{{input type="checkbox" checked=target_posts disabled=readOnly}}
|
|
||||||
{{i18n admin.badges.target_posts}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="trigger">{{i18n admin.badges.trigger}}</label>
|
|
||||||
{{view Ember.Select name="trigger" value=trigger
|
|
||||||
content=controller.badgeTriggers
|
|
||||||
optionValuePath="content.id"
|
|
||||||
optionLabelPath="content.name"
|
|
||||||
disabled=readOnly}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{{input type="checkbox" checked=allow_title}}
|
|
||||||
{{i18n admin.badges.allow_title}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{{input type="checkbox" checked=multiple_grant disabled=readOnly}}
|
|
||||||
{{i18n admin.badges.multiple_grant}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{{input type="checkbox" checked=listable disabled=readOnly}}
|
|
||||||
{{i18n admin.badges.listable}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{{input type="checkbox" checked=show_posts disabled=readOnly}}
|
|
||||||
{{i18n admin.badges.show_posts}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{{input type="checkbox" checked=enabled}}
|
|
||||||
{{i18n admin.badges.enabled}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='buttons'>
|
|
||||||
<button {{action "save"}} {{bind-attr disabled=controller.disableSave}} class='btn btn-primary'>{{i18n admin.badges.save}}</button>
|
|
||||||
<span class='saving'>{{savingStatus}}</span>
|
|
||||||
{{#unless readOnly}}
|
|
||||||
<a {{action "destroy"}} class='delete-link'>{{i18n admin.badges.delete}}</a>
|
|
||||||
{{/unless}}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if grant_count}}
|
|
||||||
<div class="span13 current-badge-actions">
|
|
||||||
<div>
|
|
||||||
{{#link-to 'badges.show' this}}{{i18n badges.granted count=grant_count}}{{/link-to}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/with}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export default Ember.View.extend(Discourse.ScrollTop);
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default Ember.View.extend(Discourse.ScrollTop, {
|
||||||
|
_scrollOnModelChange: function() {
|
||||||
|
this._scrollTop();
|
||||||
|
}.observes('controller.model.id')
|
||||||
|
});
|
|
@ -110,45 +110,24 @@ Discourse.Badge = Discourse.Model.extend({
|
||||||
@method save
|
@method save
|
||||||
@returns {Promise} A promise that resolves to the updated `Discourse.Badge`
|
@returns {Promise} A promise that resolves to the updated `Discourse.Badge`
|
||||||
**/
|
**/
|
||||||
save: function(fields) {
|
save: function(data) {
|
||||||
this.set('savingStatus', I18n.t('saving'));
|
|
||||||
this.set('saving', true);
|
|
||||||
|
|
||||||
var url = "/admin/badges",
|
var url = "/admin/badges",
|
||||||
requestType = "POST",
|
requestType = "POST",
|
||||||
self = this;
|
self = this;
|
||||||
|
|
||||||
if (!this.get('newBadge')) {
|
if (this.get('id')) {
|
||||||
// We are updating an existing badge.
|
// We are updating an existing badge.
|
||||||
url += "/" + this.get('id');
|
url += "/" + this.get('id');
|
||||||
requestType = "PUT";
|
requestType = "PUT";
|
||||||
}
|
}
|
||||||
|
|
||||||
var boolFields = ['allow_title', 'multiple_grant',
|
|
||||||
'listable', 'auto_revoke',
|
|
||||||
'enabled', 'show_posts',
|
|
||||||
'target_posts' ];
|
|
||||||
|
|
||||||
var data = {};
|
|
||||||
fields.forEach(function(field){
|
|
||||||
var d = self.get(field);
|
|
||||||
if(_.include(boolFields, field)) {
|
|
||||||
d = !!d;
|
|
||||||
}
|
|
||||||
data[field] = d;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Discourse.ajax(url, {
|
return Discourse.ajax(url, {
|
||||||
type: requestType,
|
type: requestType,
|
||||||
data: data
|
data: data
|
||||||
}).then(function(json) {
|
}).then(function(json) {
|
||||||
self.updateFromJson(json);
|
self.updateFromJson(json);
|
||||||
self.set('savingStatus', I18n.t('saved'));
|
|
||||||
self.set('saving', false);
|
|
||||||
return self;
|
return self;
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
self.set('savingStatus', I18n.t('failed'));
|
|
||||||
self.set('saving', false);
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,12 @@ class Admin::BadgesController < Admin::AdminController
|
||||||
trigger: params[:trigger].to_i)
|
trigger: params[:trigger].to_i)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
def badge_types
|
def badge_types
|
||||||
badge_types = BadgeType.all.to_a
|
badge_types = BadgeType.all.to_a
|
||||||
render_serialized(badge_types, BadgeTypeSerializer, root: "badge_types")
|
render_serialized(badge_types, BadgeTypeSerializer, root: "badge_types")
|
||||||
|
@ -98,7 +104,7 @@ class Admin::BadgesController < Admin::AdminController
|
||||||
begin
|
begin
|
||||||
BadgeGranter.contract_checks!(badge.query, { target_posts: badge.target_posts, trigger: badge.trigger })
|
BadgeGranter.contract_checks!(badge.query, { target_posts: badge.target_posts, trigger: badge.trigger })
|
||||||
rescue => e
|
rescue => e
|
||||||
errors << [e.message]
|
errors << e.message
|
||||||
raise ActiveRecord::Rollback
|
raise ActiveRecord::Rollback
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -106,10 +112,9 @@ class Admin::BadgesController < Admin::AdminController
|
||||||
badge.save!
|
badge.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
if badge.errors
|
errors
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
errors.push(*badge.errors.full_messages)
|
errors.push(*badge.errors.full_messages)
|
||||||
end
|
|
||||||
|
|
||||||
errors
|
errors
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2095,6 +2095,7 @@ en:
|
||||||
grant: Grant
|
grant: Grant
|
||||||
no_user_badges: "%{name} has not been granted any badges."
|
no_user_badges: "%{name} has not been granted any badges."
|
||||||
no_badges: There are no badges that can be granted.
|
no_badges: There are no badges that can be granted.
|
||||||
|
none_selected: "Select a badge to get started"
|
||||||
allow_title: Allow badge to be used as a title
|
allow_title: Allow badge to be used as a title
|
||||||
multiple_grant: Can be granted multiple times
|
multiple_grant: Can be granted multiple times
|
||||||
listable: Show badge on the public badges page
|
listable: Show badge on the public badges page
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
moduleFor("controller:admin-badges", "controller:admin-badges", {
|
|
||||||
needs: ['controller:modal', 'controller:admin-badge']
|
|
||||||
});
|
|
||||||
|
|
||||||
test("canEditDescription", function() {
|
|
||||||
var badge = Discourse.Badge.create({id: 101, name: "Test Badge"});
|
|
||||||
var controller = this.subject({ model: [badge] });
|
|
||||||
controller.send('selectBadge', badge);
|
|
||||||
ok(controller.get('canEditDescription'), "allows editing description when a translation exists for the badge name");
|
|
||||||
|
|
||||||
badge.set('translatedDescription', 'translated');
|
|
||||||
ok(!controller.get('canEditDescription'), "can't edit the description when it's got a translation");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("createNewBadge", function() {
|
|
||||||
var controller = this.subject();
|
|
||||||
controller.send('createNewBadge');
|
|
||||||
equal(controller.get('model.length'), 1, "adds a new badge to the list of badges");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("selectBadge", function() {
|
|
||||||
var badge = Discourse.Badge.create({id: 101, name: "Test Badge"}),
|
|
||||||
controller = this.subject({ model: [badge] });
|
|
||||||
|
|
||||||
controller.send('selectBadge', badge);
|
|
||||||
equal(controller.get('selectedItem'), badge, "the badge is selected");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save", function() {
|
|
||||||
var badge = Discourse.Badge.create({id: 101, name: "Test Badge"}),
|
|
||||||
otherBadge = Discourse.Badge.create({id: 102, name: "Other Badge"}),
|
|
||||||
controller = this.subject({ model: [badge, otherBadge] });
|
|
||||||
|
|
||||||
controller.send('selectBadge', badge);
|
|
||||||
sandbox.stub(badge, "save").returns(Ember.RSVP.resolve({}));
|
|
||||||
controller.send("save");
|
|
||||||
ok(badge.save.calledOnce, "called save on the badge");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("destroy", function() {
|
|
||||||
var badge = Discourse.Badge.create({id: 101, name: "Test Badge"}),
|
|
||||||
otherBadge = Discourse.Badge.create({id: 102, name: "Other Badge"}),
|
|
||||||
controller = this.subject({model: [badge, otherBadge]});
|
|
||||||
|
|
||||||
sandbox.stub(badge, 'destroy').returns(Ember.RSVP.resolve({}));
|
|
||||||
|
|
||||||
bootbox.confirm = function(text, yes, no, func) {
|
|
||||||
func(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
controller.send('selectBadge', badge);
|
|
||||||
controller.send('destroy');
|
|
||||||
ok(!badge.destroy.calledOnce, "badge is not destroyed if they user clicks no");
|
|
||||||
|
|
||||||
bootbox.confirm = function(text, yes, no, func) {
|
|
||||||
func(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
controller.send('selectBadge', badge);
|
|
||||||
controller.send('destroy');
|
|
||||||
ok(badge.destroy.calledOnce, "badge is destroyed if they user clicks yes");
|
|
||||||
});
|
|
Loading…
Reference in a new issue