From 667888aeb359920cd4937567da20d87dcf3267da Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Sun, 23 Feb 2014 12:32:35 +0000 Subject: [PATCH] Implements Initial lifecycle and App UI start Closes #2083 * Added hbs template for apps listing * Added settings to read the activeApps * Added viewcontrol to activate / deactivate apps * Added API handler to store activeApps (by `name` in the `package.json` file) * On button click it turns the button into "Working" and changes class to `button` (grey one) * On success, rerenders the pane, adds success notification about apps being saved * On error, rerenders the pane, adds error notification with error message Missing: * tests: couldn't figure out how to add mock apps with mock package.json data * actually registering, etc, re #2140 * icon from the sidebar --- core/client/models/settings.js | 2 +- core/client/tpl/settings/apps.hbs | 15 ++++ core/client/tpl/settings/sidebar.hbs | 1 + core/client/views/settings.js | 66 ++++++++++++++++ core/server/api/settings.js | 85 +++++++++++++++------ core/server/controllers/admin.js | 2 +- core/test/functional/admin/settings_test.js | 5 +- core/test/utils/api.js | 2 +- 8 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 core/client/tpl/settings/apps.hbs 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 @@ +
+ +

Apps

+
+ +
+ +
\ 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',