From 0810b806f7605a75b080ea4a6fc9010c6552095c Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Sat, 11 Apr 2015 01:41:34 -0400 Subject: Implement admin interface resource reordering --- examples/db.json | 63 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 10 deletions(-) (limited to 'examples') diff --git a/examples/db.json b/examples/db.json index 148391a..20d4510 100644 --- a/examples/db.json +++ b/examples/db.json @@ -3,8 +3,8 @@ "bread": [ { "type": "pretzel", - "description": "really a very tasty bread! yup yes", - "color": "green", + "description": "really a very tasty bread!", + "color": "red", "id": "pretzel", "title": "Pretzel Chips", "images": [ @@ -23,7 +23,8 @@ "token": "", "title": "", "thumb": "" - } + }, + "__index": 0 }, { "type": "bagel", @@ -38,31 +39,73 @@ "token": "112498725", "title": "FW14-2H-VIDEO-V4 2", "thumb": "http://i.vimeocdn.com/video/497493142_640.jpg" - } + }, + "__index": 1 }, { "type": "pumpernickel", - "description": "grandma's recipe", + "description": "yup", "id": "pumpernickel", - "title": "Pumpernickel", + "title": "ok", "images": [ { "uri": "cool", "caption": "cool" } - ] + ], + "color": "red", + "video": { + "url": "", + "type": "", + "token": "", + "title": "", + "thumb": "" + }, + "__index": 2 + }, + { + "type": "cracker", + "title": "", + "description": "once upon a time this noble creature etc", + "color": "red", + "video": { + "url": "", + "type": "", + "token": "", + "title": "", + "thumb": "" + }, + "__index": 4, + "id": "cracker" + }, + { + "type": "croissant", + "title": "", + "description": "wow just wow", + "color": "red", + "video": { + "url": "", + "type": "", + "token": "", + "title": "", + "thumb": "" + }, + "__index": 3, + "id": "croissant" } ], "page": [ { "title": "About Us", "body": "Just a small bakery", - "id": "about" + "id": "about", + "__index": 1 }, { - "title": "contact", + "title": "ok...", "body": "2406 Old Rd, San Juan Bautista", - "id": "contact" + "id": "contact", + "__index": 0 } ] } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 5b7e5a133e5326a98b974b7e9e390ac393d1a05e Mon Sep 17 00:00:00 2001 From: Sean Fridman Date: Mon, 13 Apr 2015 13:42:11 -0400 Subject: Client friendly error handling --- app/index.js | 64 ++++++++++++++-- app/node_modules/okadminview/index.js | 125 ++++++++++++++------------------ app/node_modules/okserver/index.js | 7 ++ app/node_modules/okview/index.js | 133 +++++++++++++++------------------- examples/index.js | 2 + themes/okadmin/templates/404.liquid | 54 ++++++++++++++ themes/okadmin/templates/5xx.liquid | 54 ++++++++++++++ 7 files changed, 289 insertions(+), 150 deletions(-) create mode 100644 themes/okadmin/templates/404.liquid create mode 100644 themes/okadmin/templates/5xx.liquid (limited to 'examples') diff --git a/app/index.js b/app/index.js index 719c424..419adfc 100644 --- a/app/index.js +++ b/app/index.js @@ -36,6 +36,7 @@ function OKCMS(options) { var templateRoot = options.templateRoot || 'templates'; var adminTemplateRoot = options.templateRoot || path.join(__dirname, '../themes/okadmin/templates'); + var debug = options.debug || false; // Set metadata defaults // TODO Abstract this out somewhere else @@ -84,13 +85,17 @@ function OKCMS(options) { }); var resourceCache = this._resourceCache = this._createResources(resourceConfig, db, schemas); + var errorHandler = createErrorHandlerProducer( + templateProvider, adminTemplateProvider, debug); // Create view instances from config var views = this._views = - this._createViews(viewConfig, db, meta, resourceCache, templateProvider); + this._createViews(viewConfig, db, meta, resourceCache, templateProvider, + errorHandler); var adminViews = this._adminViews = this._createAdminViews(adminPath, app, express, resourceConfig, - resourceCache, adminTemplateProvider, adminMeta); + resourceCache, adminTemplateProvider, adminMeta, + errorHandler); // Create services var imageService = OKImageService({ @@ -109,7 +114,8 @@ function OKCMS(options) { adminPath: adminPath, services: { image: imageService - } + }, + errorHandler: errorHandler }); } @@ -152,7 +158,7 @@ OKCMS.prototype._createResources = function(resourceConfig, db, schemaCache) { }; OKCMS.prototype._createViews = function(viewConfig, db, - meta, resourceCache, templateProvider) { + meta, resourceCache, templateProvider, errorHandler) { viewConfig = viewConfig || {}; var self = this; var createQueries = this._createQueries.bind(this); @@ -171,7 +177,8 @@ OKCMS.prototype._createViews = function(viewConfig, db, route: route, template: template, queries: queries, - meta: meta + meta: meta, + errorHandler: errorHandler }); return cache; }, {}); @@ -193,7 +200,8 @@ OKCMS.prototype._createViews = function(viewConfig, db, }; OKCMS.prototype._createAdminViews = function(path, app, express, - resourceConfig, resourceCache, templateProvider, meta) { + resourceConfig, resourceCache, templateProvider, meta, + errorHandler) { var views = {}; var withTrail = withTrailingSlash(path); var withoutTrail = withoutTrailingSlash(path); @@ -217,7 +225,8 @@ OKCMS.prototype._createAdminViews = function(path, app, express, resourceConfig: resourceConfig, resourceCache: resourceCache, templateProvider: templateProvider, - meta: meta + meta: meta, + errorHandler: errorHandler }); return views; }; @@ -269,6 +278,47 @@ ResourceCache.prototype.get = function(type, id) { } }; +/** + * Higher order function implementing customizable error handling + */ +function createErrorHandlerProducer(templateProvider, adminTemplateProvider, debugMode) { + + var template404 = templateProvider.getTemplate('404') || + adminTemplateProvider.getTemplate('404'); + var template5xx = templateProvider.getTemplate('5xx') || + adminTemplateProvider.getTemplate('5xx'); + + if (!template404 || !template5xx) + throw new Error('No error templates provided by admin theme or user') + + return debugMode ? createDebugHandler : createErrorHandler; + + function createErrorHandler(req, res, status) { + return function handleError(err) { + switch (status) { + case 404: + template404.render().then(function(rendered) { + res.status(status); + res.send(rendered); + }); + break; + default: + template5xx.render().then(function(rendered) { + res.status(status); + res.send(rendered); + }); + } + + }; + } + + function createDebugHandler(req, res, status) { + return function handleError(err) { + res.send(err.stack); + }; + } +} + module.exports = { createApp: function(options) { diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js index 2f31e1e..648487c 100644 --- a/app/node_modules/okadminview/index.js +++ b/app/node_modules/okadminview/index.js @@ -37,6 +37,8 @@ function OKAdminView(options) { throw new Error('No templateProvider provided to OKAdminView'); if (!options.meta) throw new Error('No meta query provided to OKAdminView'); + if (!options.errorHandler) + throw new Error('No error handler provided to OKAdminView'); var app = options.app; var express = options.express; @@ -44,6 +46,7 @@ function OKAdminView(options) { var resourceCache = this._resourceCache = options.resourceCache; var resourceConfig = this._resourceConfig = cloneDeep(options.resourceConfig); var provider = options.templateProvider; + var error = this._error = options.errorHandler; // Load templates var templates = this._templates = @@ -112,12 +115,10 @@ function OKAdminView(options) { } })); - var auth = passport.authenticate('digest', {session: false}); - // This should really be mounted on the router, but can't be due to // https://github.com/jaredhanson/passport-http/pull/16 app.use('/admin/', passport.initialize()); - app.all('/admin/:path*', auth); + app.all('/admin/:path*', passport.authenticate('digest', {session: false})); router.get('/', function readIndex(req, res, next) { fetchIndexTemplateData(meta, indexQueries).then(function(data) { @@ -125,22 +126,22 @@ function OKAdminView(options) { success: req.flash('success'), errors: req.flash('errors') })); - }).fail(errorHandler(req, res)); + }).fail(error(req, res, 500)); }); 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)); + error(req, res, 404)(new Error('No such resource ' + type)); } else { meta.get().then(function(metadata) { - var templateData = getResourceTemplateData(metadata, resource, {}); + var templateData = transformData(metadata, resource, {}); view.renderResourceNew(req, res, assign(templateData, { success: req.flash('success'), errors: req.flash('errors'), })); - }).fail(errorHandler(req, res)); + }).fail(error(req, res, 500)); } }); @@ -148,26 +149,27 @@ function OKAdminView(options) { 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, getResourceTemplateData) - .then(function(data) { - if (!data) { - resourceMissingHandler(req, res)() - } else { - view.renderResource(req, res, assign(data, { - success: req.flash('success'), - errors: req.flash('errors') - })); - } - }).fail(errorHandler(req, res)); - } - + var query = OKQuery({ + resource: resource, + query: id + }); + fetchResourceTemplateData(meta, query, transformData) + .then(function(data) { + if (!data) { + resourceMissingHandler(req, res)() + } else { + view.renderResource(req, res, assign(data, { + success: req.flash('success'), + errors: req.flash('errors') + })); + } + }).fail(function(err) { + if (err.message === 'No resource data') { + error(req, res, 404)(new Error('No such resource')); + } else { + error(req, res, 500)(err); + } + }); }); router.post('/:type/', function createResource(req, res, next) { @@ -175,7 +177,7 @@ function OKAdminView(options) { var resource = resourceCache.get(type); var data = req.body; if (!resource) { - errorHandler(req, res)(new Error('No such resource ' + type)); + error(req, res, 400)(new Error('No such resource ' + type)); } else { meta.get().then(function(metadata) { try { @@ -183,12 +185,12 @@ function OKAdminView(options) { resource.create(data).then(function(created) { req.flash('success', {action: 'create'}); res.redirect(303, resource.getID(data)); - }).fail(errorHandler(req, res)); + }).fail(error(req, res, 500)); } catch (errors) { - var templateData = getResourceTemplateData(metadata, resource, data); + var templateData = transformData(metadata, resource, data); view.renderResource(req, res, assign(templateData, {errors: errors})); } - }).fail(errorHandler(req, res));; + }).fail(error(req, res, 500));; } }); @@ -198,11 +200,9 @@ function OKAdminView(options) { var resourcesJSON = body[type]; var resource = resourceCache.get(type); if (!resourcesJSON || !resourcesJSON.length) { - res.status(400); - errorHandler(req, res)(new Error('Bad request')); + error(req, res, 400)(new Error('Bad request')); } else if (!resource) { - res.status(404); - errorHandler(req, res)(new Error('No such resource')); + error(req, res, 404)(new Error('No such resource')); } else { try { var ids = []; @@ -212,7 +212,7 @@ function OKAdminView(options) { return data; }); } catch (e) { - errorHandler(req, res)(new Error('Resource batch contains invalid JSON')); + error(req, res, 500)(new Error('Resource batch contains invalid JSON')); return; } Q.all([ @@ -222,7 +222,7 @@ function OKAdminView(options) { var metadata = results.shift(); req.flash('success', {action: 'batch_update'}); res.redirect(303, '../..'); - }).fail(errorHandler(req, res)); + }).fail(error(req, res, 500)); } }); @@ -232,7 +232,7 @@ function OKAdminView(options) { var data = req.body; var resource = resourceCache.get(type, id); if (!resource) { - errorHandler(req, res)(new Error('No such resource ' + type)); + error(req, res, 400)(new Error('No such resource ' + type)); } else { // TODO Prob should make metadata synchronous... meta.get().then(function(metadata) { @@ -241,12 +241,12 @@ function OKAdminView(options) { resource.update(id, data).then(function(updated) { req.flash('success', {action: 'update'}); res.redirect(303, '../' + resource.getID(updated)); - }).fail(errorHandler(req, res)); + }).fail(error(req, res, 500)); } catch (errors) { - var templateData = getResourceTemplateData(metadata, resource, data); + var templateData = transformData(metadata, resource, data); view.renderResource(req, res, assign(templateData, {errors: errors})); } - }).fail(errorHandler(req, res)); + }).fail(error(req, res, 500)); } }); @@ -255,14 +255,14 @@ function OKAdminView(options) { var id = req.params.id; var resource = resourceCache.get(type, id); if (!resource) { - errorHandler(req, res)(new Error('No such resource ' + type)); + error(req, res, 500)(new Error('No such resource ' + type)); } else { meta.get().then(function(metadata) { resource.destroy(id).then(function() { req.flash('success', {action: 'delete'}); res.redirect(303, '../..'); - }).fail(errorHandler(req, res)); - }).fail(errorHandler(req, res)); + }).fail(error(req, res, 500)); + }).fail(error(req, res, 500)); } }); @@ -271,9 +271,9 @@ function OKAdminView(options) { } /** - * Get template data for a single resource + * Yields formatted template data for a single resource */ -function getResourceTemplateData(meta, resource, data) { +function transformData(meta, resource, data) { meta = meta || {}; resource = resource || {}; data = data || {}; @@ -306,21 +306,21 @@ OKAdminView.prototype.renderIndex = function(req, res, data) { data = data || {}; this._templates['index'].render(data).then(function(rendered) { res.send(rendered); - }).fail(errorHandler(req, res)); + }).fail(this._error(req, res, 500)); }; OKAdminView.prototype.renderResource = function(req, res, data) { data = data || {}; this._templates['resource'].render(data).then(function(rendered) { res.send(rendered); - }).fail(errorHandler(req, res)); + }).fail(this._error(req, res, 500)); }; 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)); + }).fail(this._error(req, res, 500)); }; /** @@ -378,30 +378,15 @@ function fetchResourceTemplateData(meta, query, fn) { 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)); + if (!data) { + reject(new Error('No resource data')); + } else { + 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/okserver/index.js b/app/node_modules/okserver/index.js index cf06b3c..abca8b5 100644 --- a/app/node_modules/okserver/index.js +++ b/app/node_modules/okserver/index.js @@ -16,6 +16,8 @@ function OKServer(options) { throw new Error('No admin root directory provided to OKServer'); if (!options.adminPath) throw new Error('No admin path provided to OKServer'); + if (!options.errorHandler) + throw new Error('No error handler provided to OKServer'); var root = options.root; var adminRoot = options.adminRoot; var adminPath = options.adminPath; @@ -25,6 +27,7 @@ function OKServer(options) { var router = express.Router({ strict: app.get('strict routing') }); + var error = options.errorHandler; var services = options.services || {}; Object.keys(views) // Sort such that more general routes are matched last @@ -67,6 +70,10 @@ function OKServer(options) { // Make sure this lady is last. Checks whether the desired // route has a trailing-slash counterpart and redirects there app.use(slash()); + // Otherwise it's a 404 + app.use(function(req, res) { + error(req, res, 404)(new Error('No matching route')); + }); } OKServer.prototype.listen = function listen(port) { diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js index 2d1c0a0..63f22b5 100644 --- a/app/node_modules/okview/index.js +++ b/app/node_modules/okview/index.js @@ -24,8 +24,11 @@ function OKView(options) { throw new Error('No meta resource provided to OKView'); if (!options.route) throw new Error('No route provided to OKView'); + if (!options.errorHandler) + throw new Error('No error handler provided to OKView'); var route = options.route; var mount = options.mount || 'get'; + var error = this._error = options.errorHandler; this._template = options.template; var meta = this._meta = options.meta; var queries = this._queries = options.queries || []; @@ -47,16 +50,9 @@ function OKView(options) { enumerable: true }); - this._middleware = createMiddleware(this); - this._fetchTemplateData = unbound ? fetchUnbound : fetchBound; - - function fetchUnbound(id) { - return fetchTemplateData(meta, queries, id) - } - - function fetchBound() { - return fetchTemplateData(meta, queries); - } + this._middleware = unbound + ? unboundMiddleware(this, meta, queries, error) + : boundMiddleware(this, meta, queries, error); } OKView.prototype.middleware = function() { @@ -66,39 +62,20 @@ OKView.prototype.middleware = function() { OKView.prototype.render = function(req, res, data) { this._template.render(data).then(function(html) { res.send(html); - }).fail(errorHandler(req, res, data)); -}; - -OKView.prototype.fetchTemplateData = function() { - return this._fetchTemplateData.apply(this, arguments); + }).fail(this._error(req, res, 500)); }; -/** - * Unbound views need different middleware to resolve requests - */ -function createMiddleware(view) { - if (view.unbound) { - return unboundMiddleware(view); - } else { - return boundMiddleware(view); - } -} - -// Note that these middleware do not call next -// and should thus always be added at the end of the -// middleware chain. - /** * Creates middleware for a view which does not * yet have a resource id associated with it */ -function unboundMiddleware(view) { +function unboundMiddleware(view, meta, queries, error) { var paramName = getParamName(view.route); return function(req, res, next) { var id = req.params[paramName]; - view.fetchTemplateData(id).then(function(data) { + fetchTemplateData(meta, queries, id).then(function(data) { view.render(req, res, data); - }).fail(errorHandler(req, res, next)); + }).fail(failHandler(req, res, error)); }; } @@ -106,20 +83,22 @@ function unboundMiddleware(view) { * Creates middleware for a view which already * has a resource id associated with it */ -function boundMiddleware(view) { +function boundMiddleware(view, meta, queries, error) { return function(req, res, next) { - view.fetchTemplateData().then(function(data) { + fetchTemplateData(meta, queries).then(function(data) { view.render(req, res, data); - }).fail(errorHandler(req, res, next)); + }).fail(failHandler(req, res, error)); }; } -/** - * TODO BS error handling for now - */ -function errorHandler(req, res, next) { - return function(err) { - res.send(err.stack); +function failHandler(req, res, error) { + return function (err) { + // TODO Use custom exception type + if (err.message === 'No resource found') { + error(req, res, 404)(err); + } else { + error(req, res, 500)(err); + } } } @@ -138,9 +117,12 @@ function getParamName(route) { * and returns a promise for an object merging all queried * data, pluralizing keys where necessary. * - * Lil bit convoluted, sorry. + * Pretty convoluted, sorry. */ function fetchTemplateData(meta, queries, id) { + // If there's only one query, we assume it is for a single + // resource and will resolve errors if no data is found + var single = queries && queries.length === 1; return Q.promise(function(resolve, reject) { return Q.all( [meta.get()].concat(queries.map(function(query) { @@ -148,39 +130,44 @@ function fetchTemplateData(meta, queries, id) { }))) .then(function(results) { var metadata = results.shift(); - var normalized = results.reduce(function(cache, result, i) { - // Could be just some rogue request - if (!result) { - return cache; - } - var resource = queries[i].resource; - var type = queries[i].type; - var manyResult = isarray(result); - // Inform template of ID in generic field - if (manyResult) { - result = result.map(function(data) { - return assign({}, data, {id: resource.getID(data)}) - }); - } else { - result = assign({}, result, {id: resource.getID(result)}); - } - // If we have a lot of results for a certain type, - // we pluralize the key and yield an array of results - if (cache[type] || manyResult) { - var plural = pluralize(type); - delete cache[type]; - cache[plural] = []; + if (single && !results[0]) { + reject(new Error('No resource found')); + } else { + var normalized = results.reduce(function(cache, result, i) { + // Could be just some rogue request + if (!result) { + return cache; + } + var resource = queries[i].resource; + var type = queries[i].type; + var manyResult = isarray(result); + // Inform template of ID in generic field if (manyResult) { - cache[plural] = cache[plural].concat(result); + result = result.map(function(data) { + return assign({}, data, {id: resource.getID(data)}) + }); + } else { + result = assign({}, result, {id: resource.getID(result)}); + } + // If we have a lot of results for a certain type, + // we pluralize the key and yield an array of results + if (cache[type] || manyResult) { + var plural = pluralize(type); + delete cache[type]; + cache[plural] = []; + if (manyResult) { + cache[plural] = cache[plural].concat(result); + } else { + cache[plural].push(result); + } } else { - cache[plural].push(result); + cache[type] = result; } - } else { - cache[type] = result; - } - return cache; - }, {meta: metadata}); - resolve(normalized); + return cache; + }, {meta: metadata}); + + resolve(normalized); + } }).fail(reject); }); } diff --git a/examples/index.js b/examples/index.js index 95d2bcf..3e9f509 100644 --- a/examples/index.js +++ b/examples/index.js @@ -4,6 +4,8 @@ var app = okcms.createApp({ root: 'public', + debug: false, + schemas: { page: { id: {type: 'string'}, diff --git a/themes/okadmin/templates/404.liquid b/themes/okadmin/templates/404.liquid new file mode 100644 index 0000000..87f5342 --- /dev/null +++ b/themes/okadmin/templates/404.liquid @@ -0,0 +1,54 @@ + + + + 404 + + + +
+

¯\_(ツ)_/¯

+

We couldn't find that page.

+

Sure you have the right URL?

+ Back +
+ + diff --git a/themes/okadmin/templates/5xx.liquid b/themes/okadmin/templates/5xx.liquid new file mode 100644 index 0000000..f245545 --- /dev/null +++ b/themes/okadmin/templates/5xx.liquid @@ -0,0 +1,54 @@ + + + + 404 + + + +
+

(;一_一)

+

Looks like we experienced an error.

+

Sorry about that. Maybe try again later.

+ Back +
+ + -- cgit v1.2.3-70-g09d2 From 8898bbe48285c7f9f11760ef420cca43d683d5e0 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 28 Mar 2016 13:59:50 -0400 Subject: modify links field to specify text and uri --- app/index.js | 1 + app/node_modules/okdb/index.js | 2 +- app/node_modules/okschema/index.js | 17 +++++++- examples/db.json | 9 +++- examples/index.js | 5 ++- package.json | 2 +- themes/okadmin/public/css/main.css | 8 ++++ themes/okadmin/public/js/app.js | 34 ++++++--------- themes/okadmin/templates/partials/inputs.liquid | 56 +++++++++++++++++++------ 9 files changed, 93 insertions(+), 41 deletions(-) (limited to 'examples') diff --git a/app/index.js b/app/index.js index 6a1c74f..06b1591 100644 --- a/app/index.js +++ b/app/index.js @@ -28,6 +28,7 @@ function OKCMS(options) { var app = express(); app.enable('strict routing'); + app.disable('x-powered-by'); var root = this._root = options.root || 'public'; var adminConfig = options.admin || {}; diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js index 4820c8c..ad8d9a7 100644 --- a/app/node_modules/okdb/index.js +++ b/app/node_modules/okdb/index.js @@ -97,7 +97,7 @@ FSDB.prototype.update = function(collection, id, data) { } var result = chain.assign(cloneDeep(data)).value(); - if (result ) { + if (result) { return resolve(cloneDeep(result)); } else { return resolve(null, new Error('Problem updating document')); diff --git a/app/node_modules/okschema/index.js b/app/node_modules/okschema/index.js index 330ad6b..82aa13f 100644 --- a/app/node_modules/okschema/index.js +++ b/app/node_modules/okschema/index.js @@ -63,8 +63,21 @@ var types = { assertValid: function(spec, value) {} }, 'tag-list': { - parent: 'string', - assertValid: function(spec, value) {} + parent: [{ + uri: { type: 'string' }, + text: { type: 'string' } + }], + assertValid: function(spec, value) { + var message; + var actual; + if (!value || !value.length) { + throw [{ + message: 'Not an array', + expected: JSON.stringify(this.parent), + actual: value + }]; + } + } }, 'date': { parent: 'string', diff --git a/examples/db.json b/examples/db.json index 20d4510..4319237 100644 --- a/examples/db.json +++ b/examples/db.json @@ -99,7 +99,14 @@ "title": "About Us", "body": "Just a small bakery", "id": "about", - "__index": 1 + "__index": "1", + "links": [ + { + "text": "asdf2", + "uri": "asdf" + } + ], + "dateCreated": "" }, { "title": "ok...", diff --git a/examples/index.js b/examples/index.js index 3e9f509..bf51dfc 100644 --- a/examples/index.js +++ b/examples/index.js @@ -10,7 +10,8 @@ var app = okcms.createApp({ page: { id: {type: 'string'}, title: {type: 'string'}, - body: {type: 'text'} + body: {type: 'text'}, + links: {type: 'link-list'}, }, bread: { type: {type: 'string', id: true}, @@ -19,7 +20,7 @@ var app = okcms.createApp({ color: {type: 'enum', options: ["red","blue","green"]}, video: {type: 'video'}, images: {type: 'captioned-image-list'} - } + }, }, resources: [ diff --git a/package.json b/package.json index 192685c..faf3cc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "okcms", - "version": "0.1.18", + "version": "0.1.19", "description": "great", "main": "app/index.js", "scripts": { diff --git a/themes/okadmin/public/css/main.css b/themes/okadmin/public/css/main.css index a5fbdeb..9289fdf 100644 --- a/themes/okadmin/public/css/main.css +++ b/themes/okadmin/public/css/main.css @@ -305,6 +305,14 @@ button, input[type=submit] { .main.resource form .video-element input[type=text] { width: 15em; } +.main.resource form .group input[type=text].link-input, +.main.resource form .group input[type=text].link-input-new { + width: 13.05em; + padding: 0 0 0 0.5em; +} +.main.resource form .links li { + height: auto; +} .main .link-list .add-link-btn, .main .link-list .remove-link-btn { margin: 0; diff --git a/themes/okadmin/public/js/app.js b/themes/okadmin/public/js/app.js index edf9980..17b35d0 100644 --- a/themes/okadmin/public/js/app.js +++ b/themes/okadmin/public/js/app.js @@ -120,35 +120,25 @@ var OKAdmin = function(){ e.stopPropagation() var $delegate = $(e.delegateTarget) var $list = $delegate.find('.links') - var length = $list.find('input').length - var name = $delegate.parent('.property').data('name') - var $new = $delegate.find('.link-input-new') - var input = document.createElement('input') - var delBtn = document.createElement('button') - var inputName = name + '[' + length + ']' - $(input).attr({ - name: inputName, - type: 'text', - value: $new.val() - }) - $list.append(input) - $(delBtn).addClass('remove-link-btn') - $(delBtn).data('for', inputName) - delBtn.innerHTML = '-' - $list.append(delBtn) - $new.val('') + var $linkText = $delegate.find(".link-input-new.link-text") + var $linkURI = $delegate.find(".link-input-new.link-uri") + + var template = $delegate.find(".link-template").html() + var $el = $(template) + $el.find(".link-text").val( $linkText.val() ) + $el.find(".link-uri").val( $linkURI.val() ) + $list.append($el) + console.log($list, template) + $linkText.val("") + $linkURI.val("") }) // Remove a link from the list $('.link-list').on('click', '.remove-link-btn', function(e) { e.preventDefault() e.stopPropagation() - var $delegate = $(e.delegateTarget) var $target = $(e.target) - var inputName = $target.data('for') - var $input = $delegate.find('[name="' + inputName + '"]') - $input.remove() - $target.remove() + $target.closest("li").remove() }) // fix post indexing in list-driven inputs diff --git a/themes/okadmin/templates/partials/inputs.liquid b/themes/okadmin/templates/partials/inputs.liquid index c6efc68..55d5fb8 100644 --- a/themes/okadmin/templates/partials/inputs.liquid +++ b/themes/okadmin/templates/partials/inputs.liquid @@ -79,26 +79,58 @@ value="{{spec.value}}" placeholder="Enter a comma separated list of tags."> - {% elsif type == 'link-list' %} + {% elsif type == 'link-list' %} {% elsif type == 'media-list' %} -- cgit v1.2.3-70-g09d2 From ea9a94f3e1f980153588da1aee3fc7e456e292b5 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 28 Mar 2016 14:39:44 -0400 Subject: okschema: replace array-type data with empty list if unspecified (due to constraint of querystring body-parser) --- app/node_modules/okschema/index.js | 74 ++++++++++++++++++++++++++------------ examples/db.json | 23 ++++++++---- 2 files changed, 68 insertions(+), 29 deletions(-) (limited to 'examples') diff --git a/app/node_modules/okschema/index.js b/app/node_modules/okschema/index.js index 82aa13f..cf041fb 100644 --- a/app/node_modules/okschema/index.js +++ b/app/node_modules/okschema/index.js @@ -41,43 +41,25 @@ var types = { } }, 'captioned-image-list': { + isArray: true, parent: [{ uri: { type: 'string' }, // TODO Implement URI type caption: { type: 'string' } }], - assertValid: function(spec, value) { - var message; - var actual; - if (!value || !value.length) { - throw [{ - message: 'Not an array', - expected: JSON.stringify(this.parent), - actual: value - }]; - } - } + assertValid: function(spec, value) {} }, // Special type for resource meta information 'meta': { parent: 'string', assertValid: function(spec, value) {} }, - 'tag-list': { + 'link-list': { + isArray: true, parent: [{ uri: { type: 'string' }, text: { type: 'string' } }], - assertValid: function(spec, value) { - var message; - var actual; - if (!value || !value.length) { - throw [{ - message: 'Not an array', - expected: JSON.stringify(this.parent), - actual: value - }]; - } - } + assertValid: function(spec, value) {} }, 'date': { parent: 'string', @@ -90,8 +72,38 @@ var types = { 'foreign-key': { parent: 'enum', assertValid: function(spec, value) {} + }, + 'media-list': { + isArray: true, + parent: [], + assertValid: function(spec, value) {} + }, + 'double-captioned-image-list': { + isArray: true, + parent: [], + assertValid: function(spec, value) {} + }, + 'triple-captioned-image-list': { + isArray: true, + parent: [], + assertValid: function(spec, value) {} + }, +} + +/* +function checkArrayLength (spec, value) { + var message; + var actual; + if (!value || !value.length) { + throw [{ + message: 'Not an array', + expected: JSON.stringify(this.parent), + actual: value + }]; } } +*/ + /** * OKSchema! @@ -162,9 +174,25 @@ function OKSchema(spec) { }); } +OKSchema.prototype.checkDataForMissingArrays = function(data) { + data = data || {}; + var spec = this.spec; + + // The qs body-parser module does not have a way to represent + // empty lists. If you delete all elements from a list, + // check against the spec so we know to replace with an empty list. + Object.keys(spec).forEach(function(prop){ + var type = spec[prop].type; + if (types[type] && types[type].isArray && ! data[prop]) { + data[prop] = [] + } + }) +} + OKSchema.prototype.assertValid = function(data) { data = data || {}; var spec = this.spec; + this.checkDataForMissingArrays(data) // Run through custom validators, they'll throw if invalid Object.keys(data).forEach(function(prop) { var type = spec[prop].type; diff --git a/examples/db.json b/examples/db.json index 4319237..5e10d60 100644 --- a/examples/db.json +++ b/examples/db.json @@ -92,6 +92,22 @@ }, "__index": 3, "id": "croissant" + }, + { + "type": "Test", + "title": "Testing", + "description": "", + "color": "red", + "video": { + "url": "", + "type": "", + "token": "", + "title": "", + "thumb": "" + }, + "__index": "5", + "dateCreated": "Mon, 28 Mar 2016 18:49:32 GMT", + "images": [] } ], "page": [ @@ -100,12 +116,7 @@ "body": "Just a small bakery", "id": "about", "__index": "1", - "links": [ - { - "text": "asdf2", - "uri": "asdf" - } - ], + "links": [], "dateCreated": "" }, { -- cgit v1.2.3-70-g09d2 From 49fe25c8e884c14ab3258d22240a62f0b57da490 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 28 Mar 2016 16:15:46 -0400 Subject: make links sortable --- examples/db.json | 33 ++++++++++++------------- themes/okadmin/public/css/main.css | 9 ++++++- themes/okadmin/public/js/app.js | 19 ++++++++++---- themes/okadmin/templates/partials/inputs.liquid | 19 ++++++++------ 4 files changed, 49 insertions(+), 31 deletions(-) (limited to 'examples') diff --git a/examples/db.json b/examples/db.json index 5e10d60..9ab7233 100644 --- a/examples/db.json +++ b/examples/db.json @@ -92,22 +92,6 @@ }, "__index": 3, "id": "croissant" - }, - { - "type": "Test", - "title": "Testing", - "description": "", - "color": "red", - "video": { - "url": "", - "type": "", - "token": "", - "title": "", - "thumb": "" - }, - "__index": "5", - "dateCreated": "Mon, 28 Mar 2016 18:49:32 GMT", - "images": [] } ], "page": [ @@ -123,7 +107,22 @@ "title": "ok...", "body": "2406 Old Rd, San Juan Bautista", "id": "contact", - "__index": 0 + "__index": "0", + "links": [ + { + "text": "US Bread Board", + "uri": "http://bread.com/" + }, + { + "text": "National Council on Grain", + "uri": "http://grain.org/" + }, + { + "text": "USDA", + "uri": "http://usda.gov/" + } + ], + "dateCreated": "" } ] } \ No newline at end of file diff --git a/themes/okadmin/public/css/main.css b/themes/okadmin/public/css/main.css index 9289fdf..df0ca43 100644 --- a/themes/okadmin/public/css/main.css +++ b/themes/okadmin/public/css/main.css @@ -310,8 +310,15 @@ button, input[type=submit] { width: 13.05em; padding: 0 0 0 0.5em; } +.handle { + display: block; + width: 1em; + height: 2em; + background: #ddd; + float: left; +} .main.resource form .links li { - height: auto; + height: 2em; } .main .link-list .add-link-btn, .main .link-list .remove-link-btn { diff --git a/themes/okadmin/public/js/app.js b/themes/okadmin/public/js/app.js index 17b35d0..da398eb 100644 --- a/themes/okadmin/public/js/app.js +++ b/themes/okadmin/public/js/app.js @@ -91,7 +91,7 @@ var OKAdmin = function(){ }) // make the region sortable with drag-and-drop - $(".media-list ol, .image-list ol").sortable() + $(".media-list ol, .image-list ol, .link-list .links").sortable() $(".media-list ol, .image-list ol").disableSelection() // populate a video field with info from our url parser @@ -115,20 +115,22 @@ var OKAdmin = function(){ })) // Add a new link to the list - $('.link-list').on('click', '.add-link-btn', function(e) { - e.preventDefault() - e.stopPropagation() + $('.link-list').on('click', '.add-link-btn', function addNewLink (e) { + e.preventDefault && e.preventDefault() + e.stopPropagation && e.stopPropagation() var $delegate = $(e.delegateTarget) var $list = $delegate.find('.links') + var linkCount = $list.find("li").length + var $linkText = $delegate.find(".link-input-new.link-text") var $linkURI = $delegate.find(".link-input-new.link-uri") var template = $delegate.find(".link-template").html() + template = template.replace(/\[\]/g, "[" + linkCount + "]") var $el = $(template) $el.find(".link-text").val( $linkText.val() ) $el.find(".link-uri").val( $linkURI.val() ) $list.append($el) - console.log($list, template) $linkText.val("") $linkURI.val("") }) @@ -195,6 +197,13 @@ var OKAdmin = function(){ input.checked = true } }) + + $(".link-list").each(function(){ + var $inputs = $(this).find(".link-input-new") + if ($inputs.eq(0).val() && $inputs.eq(1).val()) { + $(this).find(".add-link-btn").trigger("click") + } + }) $("ol").each(function(){ $("li", this).each(function(index){ diff --git a/themes/okadmin/templates/partials/inputs.liquid b/themes/okadmin/templates/partials/inputs.liquid index 55d5fb8..4acb435 100644 --- a/themes/okadmin/templates/partials/inputs.liquid +++ b/themes/okadmin/templates/partials/inputs.liquid @@ -82,17 +82,18 @@ {% elsif type == 'link-list' %}