mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-30 10:58:31 -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 @@
|
|||
/**
|
||||
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'));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
export default Ember.ArrayController.extend();
|
||||
|
|
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}}
|
||||
<li>{{#link-to 'adminUsersList'}}{{i18n admin.users.title}}{{/link-to}}</li>
|
||||
{{#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 currentUser.admin}}
|
||||
<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>
|
||||
{{#each}}
|
||||
<li>
|
||||
<a {{action "selectBadge" this}} {{bind-attr class="selected:active"}}>
|
||||
{{#link-to 'adminBadges.show' id}}
|
||||
{{badge-button badge=this}}
|
||||
{{#if newBadge}}
|
||||
<span class="list-badge">{{i18n filters.new.lower_title}}</span>
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</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>
|
||||
|
||||
{{#if selectedItem}}
|
||||
{{#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}}
|
||||
|
||||
{{outlet}}
|
||||
</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
|
||||
@returns {Promise} A promise that resolves to the updated `Discourse.Badge`
|
||||
**/
|
||||
save: function(fields) {
|
||||
this.set('savingStatus', I18n.t('saving'));
|
||||
this.set('saving', true);
|
||||
|
||||
save: function(data) {
|
||||
var url = "/admin/badges",
|
||||
requestType = "POST",
|
||||
self = this;
|
||||
|
||||
if (!this.get('newBadge')) {
|
||||
if (this.get('id')) {
|
||||
// We are updating an existing badge.
|
||||
url += "/" + this.get('id');
|
||||
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, {
|
||||
type: requestType,
|
||||
data: data
|
||||
}).then(function(json) {
|
||||
self.updateFromJson(json);
|
||||
self.set('savingStatus', I18n.t('saved'));
|
||||
self.set('saving', false);
|
||||
return self;
|
||||
}).catch(function(error) {
|
||||
self.set('savingStatus', I18n.t('failed'));
|
||||
self.set('saving', false);
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
|
|
|
@ -20,6 +20,12 @@ class Admin::BadgesController < Admin::AdminController
|
|||
trigger: params[:trigger].to_i)
|
||||
end
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def badge_types
|
||||
badge_types = BadgeType.all.to_a
|
||||
render_serialized(badge_types, BadgeTypeSerializer, root: "badge_types")
|
||||
|
@ -98,7 +104,7 @@ class Admin::BadgesController < Admin::AdminController
|
|||
begin
|
||||
BadgeGranter.contract_checks!(badge.query, { target_posts: badge.target_posts, trigger: badge.trigger })
|
||||
rescue => e
|
||||
errors << [e.message]
|
||||
errors << e.message
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
|
@ -106,10 +112,9 @@ class Admin::BadgesController < Admin::AdminController
|
|||
badge.save!
|
||||
end
|
||||
|
||||
if badge.errors
|
||||
errors
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
errors.push(*badge.errors.full_messages)
|
||||
end
|
||||
|
||||
errors
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2095,6 +2095,7 @@ en:
|
|||
grant: Grant
|
||||
no_user_badges: "%{name} has not been granted any badges."
|
||||
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
|
||||
multiple_grant: Can be granted multiple times
|
||||
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