diff --git a/core/client/models/settings.js b/core/client/models/settings.js
index b57193f35c..b6a58a177e 100644
--- a/core/client/models/settings.js
+++ b/core/client/models/settings.js
@@ -3,7 +3,7 @@
'use strict';
//id:0 is used to issue PUT requests
Ghost.Models.Settings = Ghost.ProgressModel.extend({
- url: Ghost.paths.apiRoot + '/settings/?type=blog,theme',
+ url: Ghost.paths.apiRoot + '/settings/?type=blog,theme,app',
id: '0'
});
diff --git a/core/client/tpl/settings/apps.hbs b/core/client/tpl/settings/apps.hbs
new file mode 100644
index 0000000000..e584081688
--- /dev/null
+++ b/core/client/tpl/settings/apps.hbs
@@ -0,0 +1,15 @@
+
+
+
+
+ {{#each availableApps}}
+
+ {{#if package}}{{package.name}} - {{package.version}}{{else}}{{name}} - package.json missing :({{/if}}
+ Deactivate{{else}}button-add js-button-activate">Activate{{/if}}
+
+ {{/each}}
+
+
\ No newline at end of file
diff --git a/core/client/tpl/settings/sidebar.hbs b/core/client/tpl/settings/sidebar.hbs
index 246d0882c5..ad8295818b 100644
--- a/core/client/tpl/settings/sidebar.hbs
+++ b/core/client/tpl/settings/sidebar.hbs
@@ -5,5 +5,6 @@
\ No newline at end of file
diff --git a/core/client/views/settings.js b/core/client/views/settings.js
index 24bf32bfa8..9283924307 100644
--- a/core/client/views/settings.js
+++ b/core/client/views/settings.js
@@ -446,4 +446,70 @@
}
});
+ // ### Apps page
+ Settings.apps = Settings.Pane.extend({
+ id: "apps",
+
+ events: {
+ 'click .js-button-activate': 'activateApp',
+ 'click .js-button-deactivate': 'deactivateApp'
+ },
+
+ beforeRender: function () {
+ this.availableApps = this.model.toJSON().availableApps;
+ },
+
+ activateApp: function (event) {
+ var button = $(event.currentTarget);
+
+ button.removeClass('button-add').addClass('button js-button-active').text('Working');
+
+ this.saveStates();
+ },
+
+ deactivateApp: function (event) {
+ var button = $(event.currentTarget);
+
+ button.removeClass('button-delete js-button-active').addClass('button').text('Working');
+
+ this.saveStates();
+ },
+
+ saveStates: function () {
+ var activeButtons = this.$el.find('.js-apps .js-button-active'),
+ toSave = [],
+ self = this;
+
+ _.each(activeButtons, function (app) {
+ toSave.push($(app).data('app'));
+ });
+
+ this.model.save({
+ activeApps: JSON.stringify(toSave)
+ }, {
+ success: this.saveSuccess,
+ error: this.saveError
+ }).then(function () { self.render(); });
+ },
+
+ saveSuccess: function () {
+ Ghost.notifications.addItem({
+ type: 'success',
+ message: 'Active applications updated.',
+ status: 'passive',
+ id: 'success-1100'
+ });
+ },
+
+ saveError: function (xhr) {
+ Ghost.notifications.addItem({
+ type: 'error',
+ message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
+ status: 'passive'
+ });
+ },
+
+ templateName: 'settings/apps'
+ });
+
}());
diff --git a/core/server/api/settings.js b/core/server/api/settings.js
index c77313de59..f74d67d54f 100644
--- a/core/server/api/settings.js
+++ b/core/server/api/settings.js
@@ -9,6 +9,7 @@ var _ = require('lodash'),
settingsFilter,
updateSettingsCache,
readSettingsResult,
+ filterPaths,
// Holds cached settings
settingsCache = {};
@@ -78,36 +79,69 @@ readSettingsResult = function (result) {
}
})).then(function () {
return when(config().paths.availableThemes).then(function (themes) {
- var themeKeys = Object.keys(themes),
- res = [],
- i,
- item;
- for (i = 0; i < themeKeys.length; i += 1) {
- //do not include hidden files or _messages
- if (themeKeys[i].indexOf('.') !== 0 && themeKeys[i] !== '_messages') {
- item = {};
- item.name = themeKeys[i];
- if (themes[themeKeys[i]].hasOwnProperty('package.json')) {
- item.package = themes[themeKeys[i]]['package.json'];
- } else {
- item.package = false;
- }
- //data about files currently not used
- //item.details = themes[themeKeys[i]];
- if (themeKeys[i] === settings.activeTheme.value) {
- item.active = true;
- }
- res.push(item);
- }
- }
- settings.availableThemes = {};
- settings.availableThemes.value = res;
- settings.availableThemes.type = 'theme';
+ var res = filterPaths(themes, settings.activeTheme.value);
+ settings.availableThemes = {
+ value: res,
+ type: 'theme'
+ };
+ return settings;
+ });
+ }).then(function () {
+ return when(config().paths.availableApps).then(function (apps) {
+ var res = filterPaths(apps, JSON.parse(settings.activeApps.value));
+ settings.availableApps = {
+ value: res,
+ type: 'app'
+ };
return settings;
});
});
};
+/**
+ * Normalizes paths read by require-tree so that the apps and themes modules can use them.
+ * Creates an empty array (res), and populates it with useful info about the read packages
+ * like name, whether they're active (comparison with the second argument), and if they
+ * have a package.json, that, otherwise false
+ * @param object paths as returned by require-tree()
+ * @param array/string active as read from the settings object
+ * @return array of objects with useful info about
+ * apps / themes
+ */
+filterPaths = function (paths, active) {
+ var pathKeys = Object.keys(paths),
+ res = [],
+ item;
+
+ // turn active into an array (so themes and apps can be checked the same)
+ if (!Array.isArray(active)) {
+ active = [active];
+ }
+
+ _.each(pathKeys, function (key) {
+ //do not include hidden files or _messages
+ if (key.indexOf('.') !== 0
+ && key !== '_messages'
+ && key !== 'README.md'
+ ) {
+ item = {
+ name: key
+ };
+ if (paths[key].hasOwnProperty('package.json')) {
+ item.package = paths[key]['package.json'];
+ } else {
+ item.package = false;
+ }
+
+ if (_.indexOf(active, key) !== -1) {
+ item.active = true;
+ }
+ res.push(item);
+ }
+ });
+ return res;
+};
+
settings = {
// #### Browse
@@ -153,6 +187,7 @@ settings = {
var type = key.type;
delete key.type;
delete key.availableThemes;
+ delete key.availableApps;
key = settingsCollection(key);
return dataProvider.Settings.edit(key).then(function (result) {
diff --git a/core/server/controllers/admin.js b/core/server/controllers/admin.js
index e8f2d2f19c..4f85b2a61d 100644
--- a/core/server/controllers/admin.js
+++ b/core/server/controllers/admin.js
@@ -89,7 +89,7 @@ adminControllers = {
// Method: GET
'settings': function (req, res, next) {
// TODO: Centralise list/enumeration of settings panes, so we don't run into trouble in future.
- var allowedSections = ['', 'general', 'user'],
+ var allowedSections = ['', 'general', 'user', 'app'],
section = req.url.replace(/(^\/ghost\/settings[\/]*|\/$)/ig, '');
if (allowedSections.indexOf(section) < 0) {
diff --git a/core/test/functional/admin/settings_test.js b/core/test/functional/admin/settings_test.js
index cc4e50065c..1b3fb0e55a 100644
--- a/core/test/functional/admin/settings_test.js
+++ b/core/test/functional/admin/settings_test.js
@@ -1,6 +1,6 @@
/*globals casper, __utils__, url */
-CasperTest.begin("Settings screen is correct", 15, function suite(test) {
+CasperTest.begin("Settings screen is correct", 18, function suite(test) {
casper.thenOpen(url + "ghost/settings/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
@@ -10,6 +10,9 @@ CasperTest.begin("Settings screen is correct", 15, function suite(test) {
test.assertExists(".wrapper", "Settings main view is present");
test.assertExists(".settings-sidebar", "Settings sidebar view is present");
test.assertExists(".settings-menu", "Settings menu is present");
+ test.assertExists(".settings-menu .general", "General tab is present");
+ test.assertExists(".settings-menu .users", "Users tab is present");
+ test.assertExists(".settings-menu .apps", "Apps is present");
test.assertExists(".wrapper", "Settings main view is present");
test.assertExists(".settings-content", "Settings content view is present");
test.assertEval(function testGeneralIsActive() {
diff --git a/core/test/utils/api.js b/core/test/utils/api.js
index e55b3d30bf..92a21c3e89 100644
--- a/core/test/utils/api.js
+++ b/core/test/utils/api.js
@@ -12,7 +12,7 @@ var _ = require('lodash'),
// TODO: remove databaseVersion, dbHash
settings: ['databaseVersion', 'dbHash', 'title', 'description', 'email', 'logo', 'cover', 'defaultLang',
"permalinks", 'postsPerPage', 'forceI18n', 'activeTheme', 'activeApps', 'installedApps',
- 'availableThemes', 'nextUpdateCheck', 'displayUpdateNotification'],
+ 'availableThemes', 'availableApps', 'nextUpdateCheck', 'displayUpdateNotification'],
tag: ['id', 'uuid', 'name', 'slug', 'description', 'parent_id',
'meta_title', 'meta_description', 'created_at', 'created_by', 'updated_at', 'updated_by'],
user: ['id', 'uuid', 'name', 'slug', 'email', 'image', 'cover', 'bio', 'website',