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
This commit is contained in:
Gabor Javorszky 2014-02-23 12:32:35 +00:00 committed by Hannah Wolfe
parent c64148f361
commit 667888aeb3
8 changed files with 149 additions and 29 deletions

View File

@ -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'
});

View File

@ -0,0 +1,15 @@
<header>
<button class="button-back">Back</button>
<h2 class="title">Apps</h2>
</header>
<section class="content">
<ul class="js-apps">
{{#each availableApps}}
<li>
{{#if package}}{{package.name}} - {{package.version}}{{else}}{{name}} - package.json missing :({{/if}}
<button data-app="{{name}}" class="{{#if active}}button-delete js-button-deactivate js-button-active">Deactivate{{else}}button-add js-button-activate">Activate{{/if}}</button>
</li>
{{/each}}
</ul>
</section>

View File

@ -5,5 +5,6 @@
<ul>
<li class="general"><a href="#general">General</a></li>
<li class="users"><a href="#user">User</a></li>
<li class="apps"><a href="#apps">Apps</a></li>
</ul>
</nav>

View File

@ -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'
});
}());

View File

@ -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) {

View File

@ -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) {

View File

@ -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() {

View File

@ -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',