mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 15:48:43 -05:00
FEATURE: New "Plugins" admin section with extensibility support
This commit is contained in:
parent
96b15cbba6
commit
3d7b534564
18 changed files with 208 additions and 49 deletions
|
@ -0,0 +1,12 @@
|
|||
export default Ember.Component.extend({
|
||||
tagName: 'li',
|
||||
classNameBindings: ['active'],
|
||||
|
||||
router: function() {
|
||||
return this.container.lookup('router:main');
|
||||
}.property(),
|
||||
|
||||
active: function() {
|
||||
return this.get('router').isActive(this.get('route'));
|
||||
}.property('router.url', 'route')
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
export default Ember.ArrayController.extend({
|
||||
|
||||
adminRoutes: function() {
|
||||
return this.get('model').map(p => p.admin_route).compact();
|
||||
}.property()
|
||||
});
|
12
app/assets/javascripts/admin/routes/admin-plugins.js.es6
Normal file
12
app/assets/javascripts/admin/routes/admin-plugins.js.es6
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return Discourse.ajax("/admin/plugins.json");
|
||||
},
|
||||
|
||||
actions: {
|
||||
showSettings() {
|
||||
this.transitionTo('adminSiteSettingsCategory', 'plugins');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
export default {
|
||||
resource: 'admin',
|
||||
|
||||
map: function() {
|
||||
map() {
|
||||
this.route('dashboard', { path: '/' });
|
||||
this.resource('adminSiteSettings', { path: '/site_settings' }, function() {
|
||||
this.resource('adminSiteSettingsCategory', { path: 'category/:category_id'} );
|
||||
|
|
|
@ -4,25 +4,26 @@
|
|||
<div class="full-width">
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
<li>{{#link-to 'admin.dashboard'}}{{i18n 'admin.dashboard.title'}}{{/link-to}}</li>
|
||||
{{admin-nav-item route='admin.dashboard' label='admin.dashboard.title'}}
|
||||
{{#if currentUser.admin}}
|
||||
<li>{{#link-to 'adminSiteSettings'}}{{i18n 'admin.site_settings.title'}}{{/link-to}}</li>
|
||||
{{admin-nav-item route='adminSiteSettings' label='admin.site_settings.title'}}
|
||||
{{/if}}
|
||||
<li>{{#link-to 'adminUsersList'}}{{i18n 'admin.users.title'}}{{/link-to}}</li>
|
||||
{{admin-nav-item route='adminUsersList' label='admin.users.title'}}
|
||||
{{#if showBadges}}
|
||||
<li>{{#link-to 'adminBadges.index'}}{{i18n 'admin.badges.title'}}{{/link-to}}</li>
|
||||
{{admin-nav-item route='adminBadges.index' label='admin.badges.title'}}
|
||||
{{/if}}
|
||||
{{#if currentUser.admin}}
|
||||
<li>{{#link-to 'adminGroups'}}{{i18n 'admin.groups.title'}}{{/link-to}}</li>
|
||||
{{admin-nav-item route='adminGroups' label='admin.groups.title'}}
|
||||
{{/if}}
|
||||
<li>{{#link-to 'adminEmail'}}{{i18n 'admin.email.title'}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminFlags'}}{{i18n 'admin.flags.title'}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminLogs'}}{{i18n 'admin.logs.title'}}{{/link-to}}</li>
|
||||
{{admin-nav-item route='adminEmail' label='admin.email.title'}}
|
||||
{{admin-nav-item route='adminFlags' label='admin.flags.title'}}
|
||||
{{admin-nav-item route='adminLogs' label='admin.logs.title'}}
|
||||
{{#if currentUser.admin}}
|
||||
<li>{{#link-to 'adminCustomize.colors'}}{{i18n 'admin.customize.title'}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'admin.api'}}{{i18n 'admin.api.title'}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'admin.backups'}}{{i18n 'admin.backups.title'}}{{/link-to}}</li>
|
||||
{{admin-nav-item route='adminCustomize.colors' label='admin.customize.title'}}
|
||||
{{admin-nav-item route='admin.api' label='admin.api.title'}}
|
||||
{{admin-nav-item route='admin.backups' label='admin.backups.title'}}
|
||||
{{/if}}
|
||||
{{admin-nav-item route='adminPlugins.index' label='admin.plugins.title'}}
|
||||
{{plugin-outlet "admin-menu" tagName="li"}}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{{#link-to route}}{{i18n label}}{{/link-to}}
|
36
app/assets/javascripts/admin/templates/plugins-index.hbs
Normal file
36
app/assets/javascripts/admin/templates/plugins-index.hbs
Normal file
|
@ -0,0 +1,36 @@
|
|||
{{#if length}}
|
||||
|
||||
{{d-button label="admin.plugins.change_settings"
|
||||
icon="gear"
|
||||
class='settings-button pull-right'
|
||||
action="showSettings"}}
|
||||
|
||||
<h3>{{i18n "admin.plugins.installed"}}</h3>
|
||||
|
||||
<br/>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "admin.plugins.name"}}</th>
|
||||
<th>{{i18n "admin.plugins.version"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{#each plugin in controller}}
|
||||
<tr>
|
||||
<td>
|
||||
{{#if plugin.admin_route}}
|
||||
{{#link-to plugin.admin_route.full_location}}{{plugin.name}}{{/link-to}}
|
||||
{{else}}
|
||||
{{plugin.name}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>{{plugin.version}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p>{{i18n "admin.plugins.none_installed"}}</p>
|
||||
{{/if}}
|
13
app/assets/javascripts/admin/templates/plugins.hbs
Normal file
13
app/assets/javascripts/admin/templates/plugins.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="admin-nav pull-left">
|
||||
<ul class="nav nav-stacked">
|
||||
{{admin-nav-item route='adminPlugins.index' label="admin.plugins.title"}}
|
||||
|
||||
{{#each route in adminRoutes}}
|
||||
{{admin-nav-item route=route.full_location label=route.label}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="admin-detail pull-left">
|
||||
{{outlet}}
|
||||
</div>
|
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="site-settings-nav pull-left">
|
||||
<div class="admin-nav pull-left">
|
||||
<ul class="nav nav-stacked">
|
||||
{{#each category in controller}}
|
||||
{{#link-to 'adminSiteSettingsCategory' category.nameKey tagName='li' class=category.nameKey}}
|
||||
|
@ -26,7 +26,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="site-settings-detail pull-left">
|
||||
<div class="admin-detail pull-left">
|
||||
{{outlet}}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ export default Ember.DefaultResolver.extend({
|
|||
|
||||
parseName: parseName,
|
||||
|
||||
normalize: function(fullName) {
|
||||
normalize(fullName) {
|
||||
var split = fullName.split(':');
|
||||
if (split.length > 1) {
|
||||
var discourseBase = 'discourse/' + split[0] + 's/';
|
||||
|
@ -68,14 +68,14 @@ export default Ember.DefaultResolver.extend({
|
|||
return this._super(fullName);
|
||||
},
|
||||
|
||||
customResolve: function(parsedName) {
|
||||
customResolve(parsedName) {
|
||||
// If we end with the name we want, use it. This allows us to define components within plugins.
|
||||
var suffix = parsedName.type + 's/' + parsedName.fullNameWithoutType,
|
||||
dashed = Ember.String.dasherize(suffix),
|
||||
moduleName = Ember.keys(requirejs.entries).find(function(e) {
|
||||
return (e.indexOf(suffix, e.length - suffix.length) !== -1) ||
|
||||
(e.indexOf(dashed, e.length - dashed.length) !== -1);
|
||||
});
|
||||
const suffix = parsedName.type + 's/' + parsedName.fullNameWithoutType,
|
||||
dashed = Ember.String.dasherize(suffix),
|
||||
moduleName = Ember.keys(requirejs.entries).find(function(e) {
|
||||
return (e.indexOf(suffix, e.length - suffix.length) !== -1) ||
|
||||
(e.indexOf(dashed, e.length - dashed.length) !== -1);
|
||||
});
|
||||
|
||||
var module;
|
||||
if (moduleName) {
|
||||
|
@ -85,27 +85,27 @@ export default Ember.DefaultResolver.extend({
|
|||
return module;
|
||||
},
|
||||
|
||||
resolveView: function(parsedName) {
|
||||
resolveView(parsedName) {
|
||||
return this.findLoadingView(parsedName) || this.customResolve(parsedName) || this._super(parsedName);
|
||||
},
|
||||
|
||||
resolveHelper: function(parsedName) {
|
||||
resolveHelper(parsedName) {
|
||||
return this.customResolve(parsedName) || this._super(parsedName);
|
||||
},
|
||||
|
||||
resolveController: function(parsedName) {
|
||||
resolveController(parsedName) {
|
||||
return this.customResolve(parsedName) || this._super(parsedName);
|
||||
},
|
||||
|
||||
resolveComponent: function(parsedName) {
|
||||
resolveComponent(parsedName) {
|
||||
return this.customResolve(parsedName) || this._super(parsedName);
|
||||
},
|
||||
|
||||
resolveRoute: function(parsedName) {
|
||||
resolveRoute(parsedName) {
|
||||
return this.findLoadingRoute(parsedName) || this.customResolve(parsedName) || this._super(parsedName);
|
||||
},
|
||||
|
||||
resolveTemplate: function(parsedName) {
|
||||
resolveTemplate(parsedName) {
|
||||
return this.findPluginTemplate(parsedName) ||
|
||||
this.findMobileTemplate(parsedName) ||
|
||||
this.findTemplate(parsedName) ||
|
||||
|
@ -125,23 +125,23 @@ export default Ember.DefaultResolver.extend({
|
|||
return _loadingView;
|
||||
}),
|
||||
|
||||
findPluginTemplate: function(parsedName) {
|
||||
findPluginTemplate(parsedName) {
|
||||
var pluginParsedName = this.parseName(parsedName.fullName.replace("template:", "template:javascripts/"));
|
||||
return this.findTemplate(pluginParsedName);
|
||||
},
|
||||
|
||||
findMobileTemplate: function(parsedName) {
|
||||
findMobileTemplate(parsedName) {
|
||||
if (Discourse.Mobile.mobileView) {
|
||||
var mobileParsedName = this.parseName(parsedName.fullName.replace("template:", "template:mobile/"));
|
||||
return this.findTemplate(mobileParsedName);
|
||||
}
|
||||
},
|
||||
|
||||
findTemplate: function(parsedName) {
|
||||
var withoutType = parsedName.fullNameWithoutType,
|
||||
slashedType = withoutType.replace(/\./g, '/'),
|
||||
decamelized = withoutType.decamelize(),
|
||||
templates = Ember.TEMPLATES;
|
||||
findTemplate(parsedName) {
|
||||
const withoutType = parsedName.fullNameWithoutType,
|
||||
slashedType = withoutType.replace(/\./g, '/'),
|
||||
decamelized = withoutType.decamelize(),
|
||||
templates = Ember.TEMPLATES;
|
||||
|
||||
return this._super(parsedName) ||
|
||||
templates[slashedType] ||
|
||||
|
@ -152,7 +152,7 @@ export default Ember.DefaultResolver.extend({
|
|||
this.findUnderscoredTemplate(parsedName);
|
||||
},
|
||||
|
||||
findUnderscoredTemplate: function(parsedName) {
|
||||
findUnderscoredTemplate(parsedName) {
|
||||
var decamelized = parsedName.fullNameWithoutType.decamelize();
|
||||
var underscored = decamelized.replace(/\-/g, "_");
|
||||
return Ember.TEMPLATES[underscored];
|
||||
|
@ -160,14 +160,22 @@ export default Ember.DefaultResolver.extend({
|
|||
|
||||
// Try to find a template within a special admin namespace, e.g. adminEmail => admin/templates/email
|
||||
// (similar to how discourse lays out templates)
|
||||
findAdminTemplate: function(parsedName) {
|
||||
findAdminTemplate(parsedName) {
|
||||
var decamelized = parsedName.fullNameWithoutType.decamelize();
|
||||
if (decamelized.indexOf('admin') === 0) {
|
||||
|
||||
if (decamelized.indexOf('components') === 0) {
|
||||
const compTemplate = Ember.TEMPLATES['admin/templates/' + decamelized];
|
||||
if (compTemplate) { return compTemplate; }
|
||||
}
|
||||
if (decamelized.indexOf('admin') === 0 || decamelized.indexOf('javascripts/admin') === 0) {
|
||||
decamelized = decamelized.replace(/^admin\_/, 'admin/templates/');
|
||||
decamelized = decamelized.replace(/^admin\./, 'admin/templates/');
|
||||
decamelized = decamelized.replace(/\./g, '_');
|
||||
var dashed = decamelized.replace(/_/g, '-');
|
||||
return Ember.TEMPLATES[decamelized] || Ember.TEMPLATES[dashed];
|
||||
|
||||
const dashed = decamelized.replace(/_/g, '-');
|
||||
return Ember.TEMPLATES[decamelized] ||
|
||||
Ember.TEMPLATES[dashed] ||
|
||||
Ember.TEMPLATES[dashed.replace('admin-', 'admin/')];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ export default {
|
|||
name: 'dynamic-route-builders',
|
||||
after: 'register-discourse-location',
|
||||
|
||||
initialize: function(container, app) {
|
||||
initialize(container, app) {
|
||||
app.DiscoveryCategoryRoute = buildCategoryRoute('latest');
|
||||
app.DiscoveryParentCategoryRoute = buildCategoryRoute('latest');
|
||||
app.DiscoveryCategoryNoneRoute = buildCategoryRoute('latest', {no_subcategories: true});
|
||||
|
|
|
@ -97,7 +97,8 @@ Discourse.Route.reopenClass({
|
|||
},
|
||||
|
||||
mapRoutes: function() {
|
||||
var resources = {};
|
||||
var resources = {},
|
||||
paths = {};
|
||||
|
||||
// If a module is defined as `route-map` in discourse or a plugin, its routes
|
||||
// will be built automatically. You can supply a `resource` property to
|
||||
|
@ -115,6 +116,7 @@ Discourse.Route.reopenClass({
|
|||
|
||||
if (!resources[mapObj.resource]) { resources[mapObj.resource] = []; }
|
||||
resources[mapObj.resource].push(mapObj.map);
|
||||
if (mapObj.path) { paths[mapObj.resource] = mapObj.path; }
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -129,13 +131,32 @@ Discourse.Route.reopenClass({
|
|||
delete resources.root;
|
||||
}
|
||||
|
||||
// Apply other resources next
|
||||
var segments = {},
|
||||
standalone = [];
|
||||
|
||||
Object.keys(resources).forEach(function(r) {
|
||||
router.resource(r, function() {
|
||||
var m = /^([^\.]+)\.(.*)$/.exec(r);
|
||||
if (m) {
|
||||
segments[m[1]] = m[2];
|
||||
} else {
|
||||
standalone.push(r);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply other resources next. A little hacky but works!
|
||||
standalone.forEach(function(r) {
|
||||
router.resource(r, {path: paths[r]}, function() {
|
||||
var res = this;
|
||||
resources[r].forEach(function(m) {
|
||||
m.call(res);
|
||||
});
|
||||
resources[r].forEach(function(m) { m.call(res); });
|
||||
|
||||
var s = segments[r];
|
||||
if (s) {
|
||||
var full = r + '.' + s;
|
||||
res.resource(s, {path: paths[full]}, function() {
|
||||
var nestedRes = this;
|
||||
resources[full].forEach(function(m) { m.call(nestedRes); });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -181,7 +181,7 @@ td.flaggers td {
|
|||
}
|
||||
}
|
||||
|
||||
.site-settings-nav {
|
||||
.admin-nav {
|
||||
width: 18.018%;
|
||||
margin-top: 30px;
|
||||
.nav-stacked {
|
||||
|
@ -193,7 +193,7 @@ td.flaggers td {
|
|||
}
|
||||
}
|
||||
|
||||
.site-settings-detail {
|
||||
.admin-detail {
|
||||
width: 76.5765%;
|
||||
min-height: 800px;
|
||||
margin-left: 0;
|
||||
|
|
8
app/controllers/admin/plugins_controller.rb
Normal file
8
app/controllers/admin/plugins_controller.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
class Admin::PluginsController < Admin::AdminController
|
||||
|
||||
def index
|
||||
# json = Discourse.plugins.map(&:metadata)
|
||||
render_serialized(Discourse.plugins, AdminPluginSerializer)
|
||||
end
|
||||
|
||||
end
|
26
app/serializers/admin_plugin_serializer.rb
Normal file
26
app/serializers/admin_plugin_serializer.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
class AdminPluginSerializer < ApplicationSerializer
|
||||
attributes :name,
|
||||
:version,
|
||||
:admin_route
|
||||
|
||||
def name
|
||||
object.metadata.name
|
||||
end
|
||||
|
||||
def version
|
||||
object.metadata.version
|
||||
end
|
||||
|
||||
def admin_route
|
||||
route = object.admin_route
|
||||
return unless route
|
||||
|
||||
ret = route.slice(:location, :label)
|
||||
ret[:full_location] = "adminPlugins.#{ret[:location]}"
|
||||
ret
|
||||
end
|
||||
|
||||
def include_admin_route?
|
||||
admin_route.present?
|
||||
end
|
||||
end
|
|
@ -1665,6 +1665,14 @@ en:
|
|||
all_users: "All Users"
|
||||
note_html: "Keep this key <strong>secret</strong>, all users that have it may create arbitrary posts as any user."
|
||||
|
||||
plugins:
|
||||
title: "Plugins"
|
||||
installed: "Installed Plugins"
|
||||
name: "Name"
|
||||
none_installed: "You don't have any plugins installed."
|
||||
version: "Version"
|
||||
change_settings: "Change Settings"
|
||||
|
||||
backups:
|
||||
title: "Backups"
|
||||
menu:
|
||||
|
|
|
@ -34,6 +34,8 @@ Discourse::Application.routes.draw do
|
|||
namespace :admin, constraints: StaffConstraint.new do
|
||||
get "" => "admin#index"
|
||||
|
||||
get 'plugins' => 'plugins#index'
|
||||
|
||||
resources :site_settings, constraints: AdminConstraint.new do
|
||||
collection do
|
||||
get "category/:id" => "site_settings#index"
|
||||
|
|
|
@ -6,6 +6,7 @@ require_dependency 'plugin/auth_provider'
|
|||
class Plugin::Instance
|
||||
|
||||
attr_accessor :path, :metadata
|
||||
attr_reader :admin_route
|
||||
|
||||
# Memoized array readers
|
||||
[:assets, :auth_providers, :color_schemes, :initializers, :javascripts, :styles].each do |att|
|
||||
|
@ -39,6 +40,10 @@ class Plugin::Instance
|
|||
end
|
||||
end
|
||||
|
||||
def add_admin_route(label, location)
|
||||
@admin_route = {label: label, location: location}
|
||||
end
|
||||
|
||||
def enabled?
|
||||
return @enabled_site_setting ? SiteSetting.send(@enabled_site_setting) : true
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue