summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/index.js42
-rw-r--r--app/node_modules/okadminview/index.js320
-rw-r--r--app/node_modules/okadminview/package.json17
-rw-r--r--package.json1
-rw-r--r--themes/okadmin/public/css/main.css103
-rw-r--r--themes/okadmin/templates/index.liquid26
-rw-r--r--themes/okadmin/templates/partials/head.liquid13
-rw-r--r--themes/okadmin/templates/partials/inputs.liquid19
-rw-r--r--themes/okadmin/templates/partials/tail.liquid3
-rw-r--r--themes/okadmin/templates/resource.liquid15
-rw-r--r--themes/okadmin/templates/resource_new.liquid15
11 files changed, 571 insertions, 3 deletions
diff --git a/app/index.js b/app/index.js
index 94d87dc..4ec8730 100644
--- a/app/index.js
+++ b/app/index.js
@@ -7,6 +7,7 @@ var express = require('express');
var Q = require('q');
var OKQuery = require('okquery');
var OKView = require('okview');
+var OKAdminView = require('okadminview');
var OKDB = require('okdb');
var OKResource = require('okresource')
var OKTemplate = require('oktemplate');
@@ -80,11 +81,16 @@ function OKCMS(options) {
// Create view instances from config
var views = this._views =
this._createViews(viewConfig, db, meta, resourceCache, templateProvider);
+ var adminViews = this._adminViews =
+ this._createAdminViews(adminPath, app, express, resourceConfig,
+ resourceCache, adminTemplateProvider, adminMeta);
var server = this._server = new OKServer({
express: express,
app: app,
- views: views,
+ // Merge admin views with normal views
+ views: assign(views, adminViews),
+ // Specify root folders and paths for serving static assets
root: root,
adminRoot: adminRoot,
adminPath: adminPath
@@ -166,7 +172,37 @@ OKCMS.prototype._createViews = function(viewConfig, db,
else
return '404';
}
-}
+};
+
+OKCMS.prototype._createAdminViews = function(path, app, express,
+ resourceConfig, resourceCache, templateProvider, meta) {
+ var views = {};
+ var withTrail = withTrailingSlash(path);
+ var withoutTrail = withoutTrailingSlash(path);
+ // Stoopid fix for a bug in Express. Need to do this
+ // to ensure strict routing is not broken for the nested
+ // admin router.
+ // See: https://github.com/strongloop/express/issues/2281
+ // TODO Get rid of this crap
+ views[withoutTrail] = {
+ mount: 'get',
+ middleware: function() {
+ return function(req, res) {
+ res.redirect(301, withTrail);
+ }
+ }
+ };
+ // Add real view at trailing slash route
+ views[withTrail] = OKAdminView({
+ app: app,
+ express: express,
+ resourceConfig: resourceConfig,
+ resourceCache: resourceCache,
+ templateProvider: templateProvider,
+ meta: meta
+ });
+ return views;
+};
OKCMS.prototype._createQueries = function(queryConfig, resourceCache) {
queryConfig = queryConfig || {};
@@ -218,7 +254,7 @@ ResourceCache.prototype.get = function(type, id) {
module.exports = {
createApp: function(options) {
- return new OKCMS(options);
+ return OKCMS(options);
},
OKResource: OKResource,
diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js
new file mode 100644
index 0000000..42e7f36
--- /dev/null
+++ b/app/node_modules/okadminview/index.js
@@ -0,0 +1,320 @@
+var bodyParser = require('body-parser');
+var methodOverride = require('method-override');
+var Q = require('q');
+var pluralize = require('pluralize');
+var OKQuery = require('okquery');
+
+/**
+ * OKAdminView!
+ */
+function OKAdminView(options) {
+ if (!(this instanceof OKAdminView)) return new OKAdminView(options);
+ if (!options.app)
+ throw new Error('No Express app provided to OKAdminView');
+ if (!options.express)
+ throw new Error('No Express provided to OKAdminView');
+ if (!options.resourceConfig)
+ throw new Error('No resourceConfig provided to OKAdminView');
+ if (!options.resourceCache)
+ throw new Error('No resourceCache provided to OKAdminView');
+ if (!options.templateProvider)
+ throw new Error('No templateProvider provided to OKAdminView');
+ if (!options.meta)
+ throw new Error('No meta query provided to OKAdminView');
+ var app = options.app;
+ var express = options.express;
+ var meta = options.meta;
+ var resourceCache = this._resourceCache = options.resourceCache;
+ var resourceConfig = this._resourceConfig = options.resourceConfig;
+ var provider = options.templateProvider;
+ // Load templates
+ var templates = this._templates =
+ ['index', 'resource', 'resource_new'].reduce(function(cache, name) {
+ var template = provider.getTemplate(name);
+ if (!template)
+ throw new Error('Admin theme does not have needed template: "' + name + '"');
+ cache[name] = template;
+ return cache;
+ }, {});
+ // OKAdmin middleware is a router, so mounts on 'use'
+ Object.defineProperty(this, 'mount', {
+ value: 'use',
+ writable: false,
+ enumerable: true
+ });
+
+ // Resources which apper on the index are a function of
+ // the resource configuration.
+ var indexQueries = this._indexQueries = Object.keys(resourceConfig)
+ .map(function(key) {
+ var config = resourceConfig[key];
+ var type = config.type;
+ var staticData = config.static || {};
+ var resource = resourceCache.get(config.type);
+ if (!resource)
+ throw new Error('Something weird is going on');
+ var id = staticData[resource.idField];
+ resource = resourceCache.get(type, id) || resource;
+ if (resource.bound) {
+ // Resource instances implement the query API
+ return OKQuery({resource: resource});;
+ } else {
+ return OKQuery({resource: resource, query: config.query})
+ }
+ });
+
+ this._middleware = createMiddleware(this);
+
+ function createMiddleware(view) {
+ var router = express.Router({
+ strict: app.get('strict routing')
+ });
+
+ // Parse form data
+ router.use(bodyParser.urlencoded({extended: true}));
+ // HTML forms only support POST and GET methods
+ // We extend this by adding hidden input fields to the forms which
+ // specify the actual method desired.
+ router.use(methodOverride(function(req, res) {
+ // Parse out the hidden field
+ if (req.body && typeof req.body === 'object' && '_method' in req.body) {
+ var method = req.body._method;
+ delete req.body._method
+ return method
+ }
+ }));
+
+ router.get('/', function readIndex(req, res, next) {
+ fetchIndexTemplateData(meta, indexQueries).then(function(data) {
+ view.renderIndex(req, res, data);
+ }).fail(errorHandler(req, res));
+ });
+
+ router.get('/:type/new/', function createResourceView(req, res, next) {
+ var type = req.params.type || '';
+ var resource = resourceCache.get(type);
+ if (!resource) {
+ errorHandler(req, res)(new Error('No such resource ' + type));
+ } else {
+ meta.get().then(function(metadata) {
+ view.renderResourceNew(req, res, {
+ meta: metadata,
+ resource: {
+ type: resource.type,
+ spec: resource.spec
+ }
+ });
+ }).fail(errorHandler(req, res));
+ }
+ });
+
+
+ router.get('/:type/:id/', function readResource(req, res, next) {
+ var type = req.params.type || '';
+ var id = req.params.id || '';
+ var resource = resourceCache.get(type, id);
+ if (!resource) {
+ errorHandler(req, res)(new Error('No such resource'));
+ } else {
+ var query = OKQuery({
+ resource: resource,
+ query: id
+ });
+ fetchResourceTemplateData(meta, query, transform).then(function(data) {
+ if (!data) {
+ resourceMissingHandler(req, res)()
+ } else {
+ view.renderResource(req, res, data);
+ }
+ }).fail(errorHandler(req, res));
+ }
+
+ function transform(meta, resource, data) {
+ meta = meta || {};
+ resource = resource || {};
+ data = data || {};
+ var spec = Object.keys(resource.spec).reduce(function(cache, prop) {
+ var propSpec = resource.spec[prop];
+ var value = data[prop];
+ cache[prop].id = data[resource.idField];
+ cache[prop].value = value;
+ return cache;
+ }, resource.spec);
+ return {
+ meta: meta,
+ resource: {
+ type: resource.type,
+ spec: spec
+ }
+ };
+ }
+ });
+
+ router.post('/:type/', function createResource(req, res, next) {
+ var type = req.params.type;
+ var resource = resourceCache.get(type);
+ var data = req.body;
+ if (!resource) {
+ errorHandler(req, res)(new Error('No such resource ' + type));
+ } else {
+ meta.get().then(function(metadata) {
+ var templateData = {
+ meta: metadata,
+ resource: {
+ type: resource.type,
+ spec: resource.spec,
+ data: data
+ }
+ };
+ try {
+ resource.assertValid(data);
+ resource.create(data).then(function(created) {
+ res.redirect(303, data[resource.idField]);
+ }).fail(errorHandler(req, res));
+ } catch (errors) {
+ view.renderResource(req, res, templateData);
+ }
+ }).fail(errorHandler(req, res));;
+ }
+ });
+
+ router.put('/:type/:id/', function updateResource(req, res, next) {
+ var type = req.params.type;
+ var id = req.params.id;
+ var data = req.body;
+ var resource = resourceCache.get(type, id);
+ if (!resource) {
+ errorHandler(req, res)(new Error('No such resource ' + type));
+ } else {
+ // TODO Maybe should make metadata synchronous...
+ meta.get().then(function(metadata) {
+ var templateData = {
+ meta: metadata,
+ resource: {
+ spec: resource.spec,
+ data: data
+ }
+ };
+ try {
+ resource.assertValid(data);
+ resource.update(id, data).then(function(updated) {
+ res.redirect(303, '../' + updated[resource.idField]);
+ }).fail(errorHandler(req, res));
+ } catch (errors) {
+ view.renderResource(req, res, templateData);
+ }
+ }).fail(errorHandler(req, res));
+ }
+ });
+
+ return router;
+ }
+}
+
+OKAdminView.prototype.middleware = function() {
+ return this._middleware;
+};
+
+OKAdminView.prototype.renderIndex = function(req, res, data) {
+ data = data || {};
+ this._templates['index'].render(data).then(function(rendered) {
+ res.send(rendered);
+ }).fail(errorHandler(req, res));
+};
+
+OKAdminView.prototype.renderResource = function(req, res, data) {
+ data = data || {};
+ this._templates['resource'].render(data).then(function(rendered) {
+ res.send(rendered);
+ }).fail(errorHandler(req, res));
+};
+
+OKAdminView.prototype.renderResourceNew = function(req, res, data) {
+ data = data || {meta: {}, resource: {}};
+ this._templates['resource_new'].render(data).then(function(rendered) {
+ res.send(rendered);
+ }).fail(errorHandler(req, res));
+};
+
+/**
+ * Annotate template data with schema info
+ */
+function fetchIndexTemplateData(meta, queries) {
+ return Q.Promise(function(resolve, reject) {
+ Q.all([meta.get()].concat(queries.map(function(query) {
+ return query.get();
+ }))).then(function(results) {
+ var meta = results.shift();
+ var resources = results.reduce(function(cache, result, i) {
+ if (!result)
+ return cache;
+ var resource = queries[i].resource;
+ // We want the raw object spec
+ var spec = resource.spec;
+ var key = pluralize(resource.type);
+ if (!cache[key]) {
+ cache[key] = {
+ type: resource.type,
+ spec: spec,
+ data: []
+ };
+ }
+
+ if (result.length) {
+ result.forEach(addToCache)
+ } else {
+ addToCache(result);
+ }
+
+ function addToCache(data) {
+ // Report id to template under standard name
+ data.id = data[resource.idField];
+ cache[key].data.push(data);
+ }
+
+ return cache;
+ }, {});
+
+ resolve({
+ meta: meta,
+ resources: resources
+ });
+ }).fail(reject);
+ });
+}
+
+/**
+ * Annotate template data with schema info
+ */
+function fetchResourceTemplateData(meta, query, fn) {
+ fn = fn || function(m, r, d) { return {meta: m, resource: d}; };
+ return Q.Promise(function(resolve, reject) {
+ meta.get().then(function(metadata) {
+ query.get().then(function(data) {
+ var resource = query.resource;
+ resolve(fn(metadata, resource, data));
+ }).fail(reject);
+ }).fail(reject)
+ });
+}
+
+/**
+ * TODO Real error handling
+ */
+function errorHandler(req, res) {
+ return function(err) {
+ res.send(err.stack);
+ };
+}
+
+/**
+ * TODO Real 404 handling
+ */
+function resourceMissingHandler(req, res) {
+ return function() {
+ res.status(404);
+ res.send('404');
+ }
+}
+
+module.exports = OKAdminView;
diff --git a/app/node_modules/okadminview/package.json b/app/node_modules/okadminview/package.json
new file mode 100644
index 0000000..b07cb1a
--- /dev/null
+++ b/app/node_modules/okadminview/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "okadminview",
+ "version": "1.0.0",
+ "description": "administrate!",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "OKFocus",
+ "license": "None",
+ "dependencies": {
+ "body-parser": "^1.12.2",
+ "method-override": "^2.3.2",
+ "pluralize": "^1.1.2",
+ "q": "^1.2.0"
+ }
+}
diff --git a/package.json b/package.json
index 58f04e1..1e7c1cd 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"license": "None",
"dependencies": {
"express": "^4.12.3",
+ "object-assign": "^2.0.0",
"q": "^1.2.0"
}
}
diff --git a/themes/okadmin/public/css/main.css b/themes/okadmin/public/css/main.css
new file mode 100644
index 0000000..88628e3
--- /dev/null
+++ b/themes/okadmin/public/css/main.css
@@ -0,0 +1,103 @@
+html, body {
+ margin: 0;
+ padding: 0;
+ font-family: "Helvetica", sans-serif;
+ font-size: 16px;
+}
+
+ul {
+ padding: 0;
+ list-style: none;
+}
+
+a {
+ color: #0000ff;
+ text-decoration: none;
+}
+
+a:hover {
+ border-bottom: 3px solid #0000ff;
+}
+
+a:visited {
+ color: #0000ff;
+}
+
+.admin-header {
+ height: 50px;
+ background-color: rgb(233, 233, 233);
+}
+
+.admin-header .breadcrumb {
+ margin-left: 2em;
+ font-size: 2em;
+ color: rgba(0, 0, 0, 0.25);
+ line-height: 50px;
+}
+
+.admin-header .site-link {
+ font-size: 1.5em;
+ float: right;
+ margin-right: 10%;
+ line-height: 50px;
+}
+
+.main.index .resource-category {
+ float: left;
+ min-width: 200px;
+ margin: 1em;
+ padding: 1em;
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.main.index .resource-category a.add-new {
+ border-bottom: 3px solid rgba(0, 0, 0, 0);
+ float: right;
+ font-size: 1.5em;
+ color: rgba(0, 0, 0, 0.25);
+}
+
+.main.index .resource-category li {
+ margin: 1em 0;
+}
+
+.main.index .resource-category a.add-new:hover {
+ border-bottom: 3px solid rgba(0, 0, 0, 0.25);
+}
+
+.main.resource > * {
+ margin: 1em 1em;
+}
+
+.main.resource form {
+ background-color: rgba(0, 0, 0, 0.1);
+ max-width: 500px;
+ padding: 1em;
+ font-size: 1.25em;
+}
+
+.main.resource form label {
+ display: block;
+ margin-bottom: 0.25em;
+ color: rgba(0, 0, 0, 0.75);
+}
+
+.main.resource form .property {
+ margin: 1em 0;
+}
+
+.main.resource form input {
+ display: block;
+ font-size: 1.25em;
+ min-height: 2em;
+ padding: 0 0.5em;
+}
+
+.main.resource form button {
+ font-size: 1.25em;
+ float: right;
+}
+
+.clear {
+ clear: both;
+}
diff --git a/themes/okadmin/templates/index.liquid b/themes/okadmin/templates/index.liquid
new file mode 100644
index 0000000..95c64dd
--- /dev/null
+++ b/themes/okadmin/templates/index.liquid
@@ -0,0 +1,26 @@
+{% include 'partials/head' %}
+
+<section class="index main">
+ {% for pair in resources %}
+ {% assign name = pair[0] %}
+ {% assign resource = pair[1] %}
+ {% assign spec = resource.spec %}
+
+ <section class="resource-category {{name}}">
+ <header>
+ <h2>{{name | capitalize}}</h2>
+ </header>
+ <ul class="resource-list">
+ {% for data in resource.data %}
+ <li><a href="{{resource.type}}/{{data.id}}/">{{data.id}}</a></li>
+ {% endfor %}
+ </ul>
+ <footer>
+ <a class="add-new" href="{{resource.type}}/new/">+</a>
+ </footer>
+ </section>
+
+ {% endfor %}
+</section>
+
+{% include 'partials/tail' %}
diff --git a/themes/okadmin/templates/partials/head.liquid b/themes/okadmin/templates/partials/head.liquid
new file mode 100644
index 0000000..86915a4
--- /dev/null
+++ b/themes/okadmin/templates/partials/head.liquid
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>{{meta.title}}</title>
+ <link rel="stylesheet" href="{{meta.static}}/css/main.css">
+ </head>
+ <body>
+ <header class="admin-header">
+ <span class="breadcrumb">Admin</span>
+ <a class="site-link" href="/">View Site</a>
+ </header>
+ <div class="container">
diff --git a/themes/okadmin/templates/partials/inputs.liquid b/themes/okadmin/templates/partials/inputs.liquid
new file mode 100644
index 0000000..4dd600d
--- /dev/null
+++ b/themes/okadmin/templates/partials/inputs.liquid
@@ -0,0 +1,19 @@
+{% for pair in resource.spec %}
+ {% assign name = pair[0] %}
+ {% assign spec = pair[1] %}
+ {% assign type = spec.type %}
+
+ <div class="property">
+ {% if type == 'string' %}
+ <label for="{{name}}">{{name | capitalize}}</label>
+ <input
+ {% if spec.disabled %}
+ disabled="true"
+ {% endif %}
+ name="{{name}}" type="text" value="{{spec.value}}">
+ {% else %}
+ <p><pre style="color: red">Admin template doesn't support '{{type}}' properties!</pre></p>
+ {% endif %}
+ </div>
+
+{% endfor %}
diff --git a/themes/okadmin/templates/partials/tail.liquid b/themes/okadmin/templates/partials/tail.liquid
new file mode 100644
index 0000000..773c8d4
--- /dev/null
+++ b/themes/okadmin/templates/partials/tail.liquid
@@ -0,0 +1,3 @@
+ </div> {% comment %} closes container tag {% endcomment %}
+ </body>
+</html>
diff --git a/themes/okadmin/templates/resource.liquid b/themes/okadmin/templates/resource.liquid
new file mode 100644
index 0000000..9c1b71c
--- /dev/null
+++ b/themes/okadmin/templates/resource.liquid
@@ -0,0 +1,15 @@
+{% include 'partials/head' %}
+
+<section class="resource main">
+ <nav>
+ <a href="../..">Back</a>
+ </nav>
+ <form action="." method="POST">
+ <input type="hidden" name="_method" value="PUT">
+ {% include 'partials/inputs' %}
+ <button type="submit">Save</button>
+ <div class="clear"></div>
+ </form>
+</section>
+
+{% include 'partials/tail' %}
diff --git a/themes/okadmin/templates/resource_new.liquid b/themes/okadmin/templates/resource_new.liquid
new file mode 100644
index 0000000..1e414be
--- /dev/null
+++ b/themes/okadmin/templates/resource_new.liquid
@@ -0,0 +1,15 @@
+{% include 'partials/head' %}
+
+<section class="main resource resource-new">
+ <nav>
+ <a href="../..">Back</a>
+ </nav>
+ <form action=".." method="POST">
+ <input type="hidden" name="_method" value="POST">
+ {% include 'partials/inputs' %}
+ <button type="submit">Create</button>
+ <div class="clear"></div>
+ </form>
+</section>
+
+{% include 'partials/tail' %}