diff --git a/app/assets/javascripts/admin/components/admin-nav-item.js.es6 b/app/assets/javascripts/admin/components/admin-nav-item.js.es6 new file mode 100644 index 000000000..aa1d33844 --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-nav-item.js.es6 @@ -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') +}); diff --git a/app/assets/javascripts/admin/controllers/admin-plugins.js.es6 b/app/assets/javascripts/admin/controllers/admin-plugins.js.es6 new file mode 100644 index 000000000..52b7cb6d8 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-plugins.js.es6 @@ -0,0 +1,6 @@ +export default Ember.ArrayController.extend({ + + adminRoutes: function() { + return this.get('model').map(p => p.admin_route).compact(); + }.property() +}); diff --git a/app/assets/javascripts/admin/routes/admin-plugins.js.es6 b/app/assets/javascripts/admin/routes/admin-plugins.js.es6 new file mode 100644 index 000000000..6cf5c11b5 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-plugins.js.es6 @@ -0,0 +1,12 @@ +export default Ember.Route.extend({ + model() { + return Discourse.ajax("/admin/plugins.json"); + }, + + actions: { + showSettings() { + this.transitionTo('adminSiteSettingsCategory', 'plugins'); + } + } +}); + diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index b71d9ce99..ab5f2e16c 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -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'} ); diff --git a/app/assets/javascripts/admin/templates/admin.hbs b/app/assets/javascripts/admin/templates/admin.hbs index 6a903c922..1838e268d 100644 --- a/app/assets/javascripts/admin/templates/admin.hbs +++ b/app/assets/javascripts/admin/templates/admin.hbs @@ -4,25 +4,26 @@
diff --git a/app/assets/javascripts/admin/templates/components/admin-nav-item.hbs b/app/assets/javascripts/admin/templates/components/admin-nav-item.hbs new file mode 100644 index 000000000..5ff04027b --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-nav-item.hbs @@ -0,0 +1 @@ +{{#link-to route}}{{i18n label}}{{/link-to}} diff --git a/app/assets/javascripts/admin/templates/plugins-index.hbs b/app/assets/javascripts/admin/templates/plugins-index.hbs new file mode 100644 index 000000000..e6648b975 --- /dev/null +++ b/app/assets/javascripts/admin/templates/plugins-index.hbs @@ -0,0 +1,36 @@ +{{#if length}} + + {{d-button label="admin.plugins.change_settings" + icon="gear" + class='settings-button pull-right' + action="showSettings"}} + +

{{i18n "admin.plugins.installed"}}

+ +
+ + + + + + + + + + {{#each plugin in controller}} + + + + + {{/each}} + +
{{i18n "admin.plugins.name"}}{{i18n "admin.plugins.version"}}
+ {{#if plugin.admin_route}} + {{#link-to plugin.admin_route.full_location}}{{plugin.name}}{{/link-to}} + {{else}} + {{plugin.name}} + {{/if}} + {{plugin.version}}
+{{else}} +

{{i18n "admin.plugins.none_installed"}}

+{{/if}} diff --git a/app/assets/javascripts/admin/templates/plugins.hbs b/app/assets/javascripts/admin/templates/plugins.hbs new file mode 100644 index 000000000..7150e14cd --- /dev/null +++ b/app/assets/javascripts/admin/templates/plugins.hbs @@ -0,0 +1,13 @@ +
+ +
+ +
+ {{outlet}} +
diff --git a/app/assets/javascripts/admin/templates/site_settings.hbs b/app/assets/javascripts/admin/templates/site_settings.hbs index e1bbacc1f..8587e6707 100644 --- a/app/assets/javascripts/admin/templates/site_settings.hbs +++ b/app/assets/javascripts/admin/templates/site_settings.hbs @@ -11,7 +11,7 @@
-
+
-
+
{{outlet}}
diff --git a/app/assets/javascripts/discourse/ember/resolver.js.es6 b/app/assets/javascripts/discourse/ember/resolver.js.es6 index 7976de86b..896baca33 100644 --- a/app/assets/javascripts/discourse/ember/resolver.js.es6 +++ b/app/assets/javascripts/discourse/ember/resolver.js.es6 @@ -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/')]; } } diff --git a/app/assets/javascripts/discourse/initializers/dynamic-route-builders.js.es6 b/app/assets/javascripts/discourse/initializers/dynamic-route-builders.js.es6 index a2222aa3f..64d7f8e03 100644 --- a/app/assets/javascripts/discourse/initializers/dynamic-route-builders.js.es6 +++ b/app/assets/javascripts/discourse/initializers/dynamic-route-builders.js.es6 @@ -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}); diff --git a/app/assets/javascripts/discourse/routes/discourse_route.js b/app/assets/javascripts/discourse/routes/discourse_route.js index 5d189de93..766292daa 100644 --- a/app/assets/javascripts/discourse/routes/discourse_route.js +++ b/app/assets/javascripts/discourse/routes/discourse_route.js @@ -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); }); + }); + } }); }); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 7a9e50fda..9ff7cf1f6 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -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; diff --git a/app/controllers/admin/plugins_controller.rb b/app/controllers/admin/plugins_controller.rb new file mode 100644 index 000000000..743fe97d2 --- /dev/null +++ b/app/controllers/admin/plugins_controller.rb @@ -0,0 +1,8 @@ +class Admin::PluginsController < Admin::AdminController + + def index + # json = Discourse.plugins.map(&:metadata) + render_serialized(Discourse.plugins, AdminPluginSerializer) + end + +end diff --git a/app/serializers/admin_plugin_serializer.rb b/app/serializers/admin_plugin_serializer.rb new file mode 100644 index 000000000..28b9eea7b --- /dev/null +++ b/app/serializers/admin_plugin_serializer.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f00b12b0a..0f3cc76ea 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1665,6 +1665,14 @@ en: all_users: "All Users" note_html: "Keep this key secret, 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: diff --git a/config/routes.rb b/config/routes.rb index 8a41b8be1..a6b2875f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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" diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index cb34bca3b..99de25729 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -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