summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortwo hustlers proj <jules+okfprojz@okfoc.us>2015-09-22 19:25:19 -0400
committertwo hustlers proj <jules+okfprojz@okfoc.us>2015-09-22 19:25:19 -0400
commit6b19245d0128311603d66d13c4ac606700e8912d (patch)
treecb3ea9337c76eebd2713ca18f8e844e47315a0d1
parente9fb94cd1f6e73201f0f95afde7c40cc3948ee60 (diff)
parent96eccfa594fb2526e50d4f9adfa690ea345a29ff (diff)
Merge branch 'twohustlers' of github.com:okfocus/okcms into twohustlers
-rw-r--r--app/index.js37
-rw-r--r--app/node_modules/okadminview/index.js157
-rw-r--r--app/node_modules/okquery/index.js44
-rw-r--r--app/node_modules/okresource/index.js66
-rw-r--r--app/node_modules/okschema/index.js4
-rw-r--r--app/node_modules/okservices/package.json2
-rw-r--r--app/node_modules/okview/index.js26
-rw-r--r--package.json2
-rw-r--r--themes/okadmin/public/js/app.js18
-rw-r--r--themes/okadmin/templates/index.liquid75
-rw-r--r--themes/okadmin/templates/partials/inputs.liquid2
11 files changed, 334 insertions, 99 deletions
diff --git a/app/index.js b/app/index.js
index 1c77728..2507dd2 100644
--- a/app/index.js
+++ b/app/index.js
@@ -60,6 +60,7 @@ function OKCMS(options) {
'/': { template: 'index' }
};
var serviceConfig = options.services || {};
+ var adminConfig = options.admin || {}
var templateProvider = this._templateProvider = new OKTemplate({
root: templateRoot,
@@ -77,6 +78,7 @@ function OKCMS(options) {
});
var resourceCache = this._resourceCache =
this._createResources(resourceConfig, db, schemas);
+ this._resolveForeignKeys(resourceCache)
var errorHandler = createErrorHandlerProducer(
templateProvider, adminTemplateProvider, debug);
// Create view instances from config
@@ -84,7 +86,7 @@ function OKCMS(options) {
this._createViews(viewConfig, db, meta, resourceCache, templateProvider,
errorHandler);
var adminViews = this._adminViews =
- this._createAdminViews(adminPath, app, express, resourceConfig,
+ this._createAdminViews(adminConfig, adminPath, app, express, resourceConfig,
resourceCache, adminTemplateProvider, adminMeta,
errorHandler);
@@ -153,6 +155,21 @@ OKCMS.prototype._createResources = function(resourceConfig, db, schemaCache) {
return ResourceCache(resources);
};
+OKCMS.prototype._resolveForeignKeys = function(resourceCache) {
+ resourceCache.forEach(function(resource) {
+ Object.keys(resource.foreignKeys).forEach(function(field) {
+ var foreignKeyType = resource.foreignKeys[field]
+ var keyedResource = resourceCache.get(foreignKeyType)
+ if (!keyedResource) {
+ throw new Error(format(
+ "Foreign key field '%s' in '%s' resource references unknown" +
+ "resource of type '%s'", field, resource.type, foreignKeyType))
+ }
+ resource._linkForeignKey(field, resourceCache.get(foreignKeyType))
+ })
+ })
+}
+
OKCMS.prototype._createViews = function(viewConfig, db,
meta, resourceCache, templateProvider, errorHandler) {
viewConfig = viewConfig || {};
@@ -194,9 +211,8 @@ OKCMS.prototype._createViews = function(viewConfig, db,
}
};
-OKCMS.prototype._createAdminViews = function(path, app, express,
- resourceConfig, resourceCache, templateProvider, meta,
- errorHandler) {
+OKCMS.prototype._createAdminViews = function(adminConfig, path, app, express,
+ resourceConfig, resourceCache, templateProvider, meta, errorHandler) {
var views = {};
var withTrail = withTrailingSlash(path);
var withoutTrail = withoutTrailingSlash(path);
@@ -221,7 +237,8 @@ OKCMS.prototype._createAdminViews = function(path, app, express,
resourceCache: resourceCache,
templateProvider: templateProvider,
meta: meta,
- errorHandler: errorHandler
+ errorHandler: errorHandler,
+ dashboardConfig: adminConfig.dashboard || {}
});
return views;
};
@@ -243,7 +260,8 @@ OKCMS.prototype._createQueries = function(queryConfig, resourceCache) {
query: query,
as: config.as,
sortBy: config.sortBy,
- descending: config.descending
+ descending: config.descending,
+ groupBy: config.groupBy
});
});
};
@@ -277,6 +295,13 @@ ResourceCache.prototype.get = function(type, id) {
}
};
+ResourceCache.prototype.forEach = function(cb) {
+ cb = cb || function() {}
+ Object.keys(this._cache).forEach(function(key) {
+ cb(this._cache[key])
+ }.bind(this))
+}
+
/**
* Higher order function implementing customizable error handling
*/
diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js
index 2a0fcd5..06656dc 100644
--- a/app/node_modules/okadminview/index.js
+++ b/app/node_modules/okadminview/index.js
@@ -40,6 +40,8 @@ function OKAdminView(options) {
if (!options.errorHandler)
throw new Error('No error handler provided to OKAdminView');
+ var dashboardConfig = options.dashboardConfig || {}
+ var dashboardResourceConfig = dashboardConfig.resources || {}
var app = options.app;
var express = options.express;
var meta = options.meta;
@@ -79,10 +81,25 @@ function OKAdminView(options) {
var id = resource.getID(staticData);
// Check to see if there's a more specific instance
resource = resourceCache.get(type, id) || resource;
+ var dashConf = dashboardResourceConfig[type] || {}
+ var groupBy = dashConf.groupBy
+ var sortBy = dashConf.sortBy
+ var descending = dashConf.descending
if (resource.bound) {
- return OKQuery({resource: resource});;
+ return OKQuery({
+ resource: resource,
+ groupBy: groupBy,
+ sortBy: sortBy,
+ descending: descending
+ })
} else {
- return OKQuery({resource: resource, query: config.query})
+ return OKQuery({
+ resource: resource,
+ query: config.query,
+ groupBy: groupBy,
+ sortBy: sortBy,
+ descending: descending
+ })
}
});
@@ -118,10 +135,12 @@ function OKAdminView(options) {
// 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*', passport.authenticate('digest', {session: false}));
+ app.all('/admin/(:path*)?', passport.authenticate('digest', {
+ session: false
+ }));
router.get('/', function readIndex(req, res, next) {
- fetchIndexTemplateData(meta, indexQueries).then(function(data) {
+ fetchIndexTemplateData(meta, indexQueries, dashboardConfig).then(function(data) {
view.renderIndex(req, res, assign(data, {
success: req.flash('success'),
errors: req.flash('errors')
@@ -135,11 +154,12 @@ function OKAdminView(options) {
if (!resource) {
error(req, res, 404)(new Error('No such resource ' + type));
} else {
- var templateData = transformData(meta, resource, {});
- view.renderResourceNew(req, res, assign(templateData, {
- success: req.flash('success'),
- errors: req.flash('errors'),
- }));
+ fetchNewTemplateData(meta, resource, transformData).then(function(data) {
+ view.renderResourceNew(req, res, assign(data, {
+ success: req.flash('success'),
+ errors: req.flash('errors'),
+ }))
+ }).fail(error(req, res, 500))
}
});
@@ -261,11 +281,11 @@ function OKAdminView(options) {
/**
* Yields formatted template data for a single resource
*/
-function transformData(meta, resource, data) {
+function transformData(meta, spec, resource, data) {
meta = meta || {};
resource = resource || {};
data = data || {};
- var spec = Object.keys(resource.spec).reduce(function(cache, prop) {
+ var spec = Object.keys(spec).reduce(function(cache, prop) {
var value = data[prop];
var propSpec = cache[prop];
// Decorate spec with actual resource values
@@ -275,7 +295,7 @@ function transformData(meta, resource, data) {
propSpec.hidden = true;
}
return cache;
- }, resource.spec);
+ }, spec);
return {
meta: meta,
resource: {
@@ -314,64 +334,101 @@ OKAdminView.prototype.renderResourceNew = function(req, res, data) {
/**
* Annotate template data with schema info
*/
-function fetchIndexTemplateData(meta, queries) {
+function fetchIndexTemplateData(meta, queries, dashboardConfig) {
+ var resourceConfig = dashboardConfig.resources || {}
+
return Q.promise(function(resolve, reject) {
Q.all(queries.map(function(query) {
return query.get();
})).then(function(results) {
- var resources = results.reduce(function(cache, result, i) {
- if (!result)
- return cache;
+ var templateData = results.reduce(function(acc, result, i) {
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(addData)
- } else {
- addData(result);
- }
-
- function addData(data) {
- // Report id to template under standard name
- data.id = resource.getID(data);
- cache[key].data.push(data);
+ var dashConf = resourceConfig[resource.type] || {}
+ var groupBy = dashConf.groupBy
+ var key = pluralize(resource.type)
+ acc[key] = {
+ type: resource.type,
+ spec: spec,
+ data: result,
+ groupBy: groupBy
}
-
- return cache;
- }, {});
-
+ return acc
+ }, {})
resolve({
meta: meta,
- resources: resources
- });
- }).fail(reject);
+ resources: templateData
+ })
+ }).fail(reject)
});
}
+function fetchNewTemplateData(meta, resource, transformFn) {
+ return Q.promise(function(resolve, reject) {
+ if (!resource.hasForeignKey) {
+ done({spec: resource.spec, resource: resource})
+ } else {
+ fetchForeignKeyOptions(resource).then(done).fail(reject)
+ }
+
+ function done(results) {
+ resolve(transformFn(meta, results.spec, results.resource, {}))
+ }
+ })
+}
+
/**
* Annotate template data with schema info
*/
-function fetchResourceTemplateData(meta, query, fn) {
- fn = fn || function(m, r, d) { return {meta: m, resource: d}; };
+function fetchResourceTemplateData(meta, query, transformFn) {
return Q.promise(function(resolve, reject) {
query.get().then(function(data) {
- if (!data) {
- reject(new Error('No resource data'));
+ if (!data)
+ return reject(new Error('No resource data'))
+
+ var resource = query.resource
+
+ if (resource.hasForeignKey) {
+ fetchForeignKeyOptions(resource).then(done).fail(reject)
} else {
- var resource = query.resource;
- resolve(fn(meta, resource, data));
+ done({spec: resource.spec, resource: resource})
}
- }).fail(reject);
- });
+
+ function done(results) {
+ resolve(transformFn(meta, results.spec, results.resource, data))
+ }
+ }).fail(reject)
+ })
}
+function fetchForeignKeyOptions(resource) {
+ var promises = Object.keys(resource.foreignKeys)
+ .map(fetchOptionsForKey)
+ var spec = resource.spec
+
+ return Q.all(promises).then(done)
+
+ function done() {
+ return Q.promise(function(resolve, reject) {
+ resolve({spec: spec, resource: resource})
+ })
+ }
+
+ function fetchOptionsForKey(field) {
+ var relatedResourceType = resource.foreignKeys[field]
+ return resource.related(relatedResourceType).then(fillOptions)
+
+ function fillOptions(results) {
+ return Q.promise(function(resolve, reject) {
+ spec[field].options = results.map(function(result) {
+ return result.id
+ })
+ resolve()
+ })
+ }
+ }
+}
+
+
module.exports = OKAdminView;
diff --git a/app/node_modules/okquery/index.js b/app/node_modules/okquery/index.js
index 4051f95..d4cb905 100644
--- a/app/node_modules/okquery/index.js
+++ b/app/node_modules/okquery/index.js
@@ -23,6 +23,7 @@ function OKQuery(options) {
// Queries are ordered by index by default
var sortField = options.sortBy || '__index';
+ // TODO Make descending by default
var descending = options.descending || false;
Object.defineProperty(this, 'resource', {
@@ -43,10 +44,17 @@ function OKQuery(options) {
enumerable: true
});
+ Object.defineProperty(this, 'groupBy', {
+ value: options.groupBy,
+ writable: false,
+ enumerable: true
+ })
+
this.get = createQuery(resource, query, {
default: options.default,
sortField: sortField,
- descending : descending
+ descending : descending,
+ groupBy: options.groupBy
});
}
@@ -66,6 +74,9 @@ function createQuery(resource, query, options) {
if (options.default) {
query = withDefault(query, options.default);
}
+ if (options.groupBy) {
+ query = withGrouping(query, options.groupBy)
+ }
return query;
}
@@ -124,6 +135,37 @@ function queryBound(resource) {
};
}
+/**
+ * Transform the query such that the results are grouped by the
+ * given field
+ */
+function withGrouping(queryFn, groupField) {
+ return function() {
+ return Q.Promise(function(resolve, reject) {
+ queryFn().then(function(data) {
+ data = data || []
+ if (typeof data.length === 'undefined') {
+ data = [data]
+ }
+ var result = {}
+ result[groupField] = data.reduce(reduceToGroups, {})
+ resolve(result)
+ }, reject)
+ })
+ }
+
+ function reduceToGroups(acc, data) {
+ var groupName = data[groupField]
+ if (groupName) {
+ if (!acc[groupName]) {
+ acc[groupName] = []
+ }
+ acc[groupName].push(data)
+ }
+ return acc
+ }
+}
+
function withDefault(queryFn, resultDefault) {
return function() {
return Q.Promise(function(resolve, reject) {
diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js
index c3f9adb..c1f2509 100644
--- a/app/node_modules/okresource/index.js
+++ b/app/node_modules/okresource/index.js
@@ -1,3 +1,4 @@
+var format = require('util').format
var assign = require('object-assign');
var cloneDeep = require('lodash.clonedeep');
var Q = require('q');
@@ -22,10 +23,36 @@ function OKResource(options) {
var spec = schema.spec;
var type = options.type;
+ var hasForeignKey = false
this._db = options.db;
this._schema = schema;
+ var foreignKeys = Object.keys(spec).reduce(function(acc, field) {
+ var fieldSpec = spec[field]
+ if (fieldSpec.type === 'foreign-key') {
+ hasForeignKey = true
+ acc[field] = fieldSpec.key
+ }
+ return acc
+ }, {})
+
+ // Will store references to other resources referenced via foreign keys
+ this._foreignKeyedResources = {}
+
// Define properties which are part of the API
+
+ // Should be treated as read-only
+ Object.defineProperty(this, 'foreignKeys', {
+ get: function() {
+ return foreignKeys
+ }
+ })
+
+ Object.defineProperty(this, 'hasForeignKey', {
+ get: function() {
+ return hasForeignKey
+ }
+ })
Object.defineProperty(this, 'spec', {
get: function() {
@@ -49,6 +76,30 @@ function OKResource(options) {
});
}
+OKResource.prototype._linkForeignKey = function(field, resource) {
+ this._foreignKeyedResources[field] = resource
+}
+
+/**
+ * Fetch all related resources for the given field
+ */
+OKResource.prototype.related = function(field) {
+ var resource = this._foreignKeyedResources[field]
+ return Q.promise(function(resolve, reject) {
+ if (!resource) {
+ return error(reject, new Error(format(
+ "No related resource for field '%s'", field)))
+ }
+ resource.all().then(resolve).fail(reject)
+ })
+
+ function error(reject, err) {
+ setTimeout(function() {
+ reject(err)
+ }, 0)
+ }
+}
+
/**
* Throws an error if data does not conform to schema
*/
@@ -180,6 +231,9 @@ OKResource.prototype.instance = function(options) {
});
};
+/**
+ * TODO This class is such bullshit. Refactor out
+ */
function OKResourceInstance(resource, options) {
if (!(this instanceof OKResourceInstance)) return new OKResourceInstance(options);
// Only support static data instances for now
@@ -279,6 +333,18 @@ function OKResourceInstance(resource, options) {
resource.assertValid(data);
};
+ Object.defineProperty(this, 'foreignKeys', {
+ get: function() {
+ return []
+ }
+ })
+
+ Object.defineProperty(this, 'hasForeignKey', {
+ get: function() {
+ return false
+ }
+ })
+
Object.defineProperty(this, 'parent', {
value: resource,
writable: false,
diff --git a/app/node_modules/okschema/index.js b/app/node_modules/okschema/index.js
index 89b59cc..330ad6b 100644
--- a/app/node_modules/okschema/index.js
+++ b/app/node_modules/okschema/index.js
@@ -73,6 +73,10 @@ var types = {
'flag': {
parent: 'string',
assertValid: function(spec, value) {}
+ },
+ 'foreign-key': {
+ parent: 'enum',
+ assertValid: function(spec, value) {}
}
}
diff --git a/app/node_modules/okservices/package.json b/app/node_modules/okservices/package.json
index 556b9df..2c95325 100644
--- a/app/node_modules/okservices/package.json
+++ b/app/node_modules/okservices/package.json
@@ -9,7 +9,7 @@
"author": "OKFocus",
"license": "None",
"dependencies": {
- "skipper": "git+ssh://git@github.com:nitzo/skipper#patch-1",
+ "skipper": "^0.5.7",
"skipper-s3": "^0.5.5"
}
}
diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js
index fba1c18..5f99d59 100644
--- a/app/node_modules/okview/index.js
+++ b/app/node_modules/okview/index.js
@@ -135,25 +135,28 @@ function fetchTemplateData(meta, queries, id) {
if (!result) {
return cache;
}
- var resource = queries[i].resource;
- var type = queries[i].as || queries[i].type;
+ var query = queries[i]
+ var resource = query.resource;
+ var type = query.as || query.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)});
- }
+ var groupBy = query.groupBy
// 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) {
+ if (cache[type] || manyResult || groupBy) {
var plural = pluralize(type);
delete cache[type];
cache[plural] = [];
+ // Pluralize grouped field
+ if (query.groupBy) {
+ result = Object.keys(result).reduce(function(acc, key) {
+ acc[pluralize(key)] = result[key]
+ return acc
+ }, {})
+ }
if (manyResult) {
cache[plural] = cache[plural].concat(result);
+ } else if (groupBy) {
+ cache[plural] = result
} else {
cache[plural].push(result);
}
@@ -162,7 +165,6 @@ function fetchTemplateData(meta, queries, id) {
}
return cache;
}, {meta: meta});
-
resolve(normalized);
}
}).fail(reject);
diff --git a/package.json b/package.json
index 50ec3e6..f91a986 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "okcms",
- "version": "0.1.10",
+ "version": "0.1.12",
"description": "great",
"main": "app/index.js",
"scripts": {
diff --git a/themes/okadmin/public/js/app.js b/themes/okadmin/public/js/app.js
index 4be0afc..c24605b 100644
--- a/themes/okadmin/public/js/app.js
+++ b/themes/okadmin/public/js/app.js
@@ -1,5 +1,5 @@
var OKAdmin = function(){
-
+
// initialize our multi-image uploader with an element and a template
$(".group.image-list").each(function(){
var parent = this
@@ -79,7 +79,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").disableSelection()
-
+
// populate a video field with info from our url parser
var last_url
$(".video .url").on("focus", function(){
@@ -201,7 +201,7 @@ var OKAdmin = function(){
})
})
})
-
+
// delete individual records
$("#delete_form").submit(function(e){
if (confirm("Are you sure you want to delete this record?")) {
@@ -213,9 +213,9 @@ var OKAdmin = function(){
})
// reorder items in categories
- $(".resource-category").on("click", ".edit-btn", function(e) {
+ $(".resource-category:not(.grouped)").on("click", ".edit-btn", function(e) {
e.preventDefault();
- var $parent = $(e.delegateTarget);
+ var $parent = $(e.delegateTarget)
var $editBtn = $parent.find(".edit-btn");
var $cancelBtn = $parent.find(".cancel-btn");
var $saveBtn = $parent.find(".save-btn");
@@ -245,12 +245,14 @@ var OKAdmin = function(){
});
// save new category order
- $(".resource-category").on("submit", "form", function(e) {
+ $(".resource-category.root").on("submit", "form", function(e) {
var $parent = $(e.delegateTarget);
- $parent.find(".resource-input").each(function(index) {
+ var $resources = $parent.find(".resource-input")
+ var length = $resources.length
+ $resources.each(function(index) {
var $input = $(this);
var parsed = JSON.parse($input.val());
- parsed.__index = index;
+ parsed.__index = length - index;
$input.val(JSON.stringify(parsed));
})
});
diff --git a/themes/okadmin/templates/index.liquid b/themes/okadmin/templates/index.liquid
index e9ad538..ae30bc8 100644
--- a/themes/okadmin/templates/index.liquid
+++ b/themes/okadmin/templates/index.liquid
@@ -7,33 +7,70 @@
{% assign name = pair[0] %}
{% assign resource = pair[1] %}
- <section class="resource-category {{name}}">
+ <section class="resource-category root
+ {% if resource.groupBy %} grouped {% endif %}
+ {{name}}">
<form action="{{resource.type}}/__batch__/" method="POST">
<header>
<h2>{{name | capitalize}}</h2>
</header>
- <input type="hidden" name="_method" value="PUT">
- <ol class="resource-list">
- {% for data in resource.data %}
- <li>
- <a href="{{resource.type}}/{{data.id}}/">{{data.title}}</a>
- <input class="resource-input" type="hidden" name="{{resource.type}}[{{forloop.index0}}]"
- value='{{data | stringify | escape_once}}'>
- </li>
+ <input type="hidden" name="_method" value="PUT">
+ {% if resource.groupBy %}
+ {% assign i = 0 %}
+ {% for pair in resource.data[resource.groupBy] %}
+ {% assign group = pair[0] %}
+ {% assign members = pair[1] %}
+ <section class="resource-category {{group}}">
+ <header>
+ <h2>{{group | capitalize}}</h2>
+ </header>
+ <ol class="resource-list">
+ {% for data in members %}
+ <li>
+ {% if data.disabled == 'true' %} <del> {% endif %}
+ <a href="{{resource.type}}/{{data.id}}/">{{data.title}}</a>
+ {% if data.disabled == 'true' %} </del> {% endif %}
+ <input class="resource-input" type="hidden" name="{{resource.type}}[{{i}}]"
+ value='{{data | stringify | escape_once}}'>
+ </li>
+ {% assign i = i | plus: 1 %}
+ {% endfor %}
+ </ol>
+ <footer>
+ <nav>
+ <a class="btn cancel-btn" href="#">cancel</a>
+ <button type="submit"
+ class="btn save-btn" href="#">save</button>
+ <a class="btn edit-btn active" href="#">sort</a>
+ <a class="btn add-btn active" href="{{resource.type}}/__new__/">+</a>
+ </nav>
+ </footer>
+ </section>
{% endfor %}
+ {% else %}
+ <ol class="resource-list">
+ {% for data in resource.data %}
+ <li>
+ {% if data.disabled == 'true' %} <del> {% endif %}
+ <a href="{{resource.type}}/{{data.id}}/">{{data.title}}</a>
+ {% if data.disabled == 'true' %} </del> {% endif %}
+ <input class="resource-input" type="hidden" name="{{resource.type}}[{{forloop.index0}}]"
+ value='{{data | stringify | escape_once}}'>
+ </li>
+ {% endfor %}
</ol>
- <footer>
- <nav>
- <a class="btn cancel-btn" href="#">cancel</a>
- <button type="submit"
- class="btn save-btn" href="#">save</button>
- <a class="btn edit-btn active" href="#">sort</a>
- <a class="btn add-btn active" href="{{resource.type}}/__new__/">+</a>
- </nav>
- </footer>
+ <footer>
+ <nav>
+ <a class="btn cancel-btn" href="#">cancel</a>
+ <button type="submit"
+ class="btn save-btn" href="#">save</button>
+ <a class="btn edit-btn active" href="#">sort</a>
+ <a class="btn add-btn active" href="{{resource.type}}/__new__/">+</a>
+ </nav>
+ </footer>
+ {% endif %}
</form>
</section>
-
{% endfor %}
</section>
diff --git a/themes/okadmin/templates/partials/inputs.liquid b/themes/okadmin/templates/partials/inputs.liquid
index 9c1a26b..c6efc68 100644
--- a/themes/okadmin/templates/partials/inputs.liquid
+++ b/themes/okadmin/templates/partials/inputs.liquid
@@ -17,7 +17,7 @@
<input
type="number"
name="{{name}}" value="{{spec.value}}">
- {% elsif type == 'enum' %}
+ {% elsif type == 'enum' or type == 'foreign-key' %}
<select
name="{{name}}">
{% for option in spec.options %}