How to reorder fields in Keystone's Admin UI - node.js

I'm trying to make Keystone into a CMS. So, I need models for Article, Category, ImagePage, AttachmentPage and so on. Every model I mentioned has a subset of common fields like: title, content, meta: {title, description, keywords} and so on.
In Keystone a model is constructed like this:
Article.add(fieldsCollectionObject)
so I defined the common fields in external file:
var T = require('keystone').Field.Types;
module.exports = {
title: { type: T.Text, required: true },
content: { type: T.Html, wysiwyg: true, height: 400 },
meta: {
title: { type: T.Text },
desc: { type: T.Textarea, height: 50 },
keywords: { type: T.Text },
},
publishedDate: { type: T.Date, index: true, dependsOn: { state: 'published' } },
state: { type: T.Select, options: 'draft, published, archived', default: 'draft', index: true },
};
and having require'd it in model's file I do:
const _ = require('lodash');
const pageDef = require('./common/Page.js');
const keystone = require('keystone');
const T = keystone.Field.Types;
<...>
Article.add(_.defaultsDeep({
brief: { type: T.Html, wysiwyg: true, height: 150 },
category: { type: T.Relationship, ref: 'Category', many: false, collapse: true },
tags: { type: T.Relationship, ref: 'Tag', many: true },
}, defs.authored, pageDef));
Now, the problem is with the order of fields in the Admin UI - unsurprisingly the brief, category and tags go before fields from pageDef. Is there any way to impose an order I want? Like title, brief, content, <the rest>?

defaults and defaultsDeep mutate the first object passed as a parameter to it (your initial object of Keystone fields). To have your own order, you would need to pass the objects to _.defaultsDeep in the order that you want them to appear in the object, and hence the order that they appear in the Admin UI.
Helpfully, duplicate items will not be included in the resulting object. So you would have something like this:
const _ = require('lodash');
const pageDef = require('./common/Page.js');
const keystone = require('keystone');
const T = keystone.Field.Types;
//....
let articleDef = {
brief: { type: T.Html, wysiwyg: true, height: 150 },
category: { type: T.Relationship, ref: 'Category', many: false, collapse: true },
tags: { type: T.Relationship, ref: 'Tag', many: true };
};
Article.add(_.defaultsDeep({
title: pageDef.title,
brief: articleDef.brief,
content: pageDef.content},
pageDef, articleDef));

The answer above turned out to be way to go. So I expanded and built upon it:
lib/util.js
const _ = require('lodash');
class Util {
static sourceFields (fields, ...sources) {
const source = _.defaultsDeep(...sources);
const result = [];
for (let fieldSet of fields) {
result.push(_.isArray(fieldSet) ? _.pick(source, fieldSet) : fieldSet);
}
return result;
}
}
module.exports = Util;
models/common/traits.js
var T = require('keystone').Field.Types;
module.exports = {
title: { type: T.Text, required: true },
content: { type: T.Html, wysiwyg: true, height: 400 },
indexImage: { type: T.CloudinaryImage },
meta: {
title: { type: T.Text },
desc: { type: T.Textarea, height: 50 },
keywords: { type: T.Text },
},
// <...>
}
models/Article.js
const util = require('../lib/utils.js');
const defs = require('./common/traits.js');
const keystone = require('keystone');
const T = keystone.Field.Types;
// < var Article declaration... >
const ownDef = {
brief: { type: T.Html, wysiwyg: true, height: 150 },
category: { type: T.Relationship, ref: 'Category', many: false, collapse: true },
tags: { type: T.Relationship, ref: 'Tag', many: true },
images: { type: T.Relationship, ref: 'Image', many: true, collapse: true },
attachments: { type: T.Relationship, ref: 'Attachment', many: true, collapse: true },
};
Article.add(...util.sourceFields([
'Content', ['title', 'brief', 'content', 'indexImage', 'category', 'tags'],
'Media', ['images', 'attachments'],
'SEO', ['meta'],
'Status', ['pageType', 'author', 'state', 'publishedDate'],
], ownDef, defs));
So, in traits.js I define common fields, in Article.js - fields I use only in Article model. Then, in Article model I add the fields to the List with the help of sourceFields() function. sourceFields() gets an array of fieldsets and unspecified number of field definition objects (like ownDef and defs).
The fieldset is either a string or an array of field names (keys in definition objects). If it's string it'll be a header in Admin UI, if it's array then it'll be a set of fields ordered just like field names in the array - the function basically inserts field definition into a "slot" specified in fieldset.

Related

Creating sub-module in MongoDB while loading test data from json file

I am trying to load default data into my MongoDB database from a node.js backend.
This is the data I am loading as JSON:
[
{
"datetime": "28/08/2021 16:01:00",
"sensor": {
"id": 1,
"type": "Temperature"
},
"value": 2502
},
{
"datetime": "28/08/2021 16:02:00",
"sensor": {
"id": 2,
"type": "Temperature"
},
"value": 2252
}
]
And these are the mongoose models:
const SensorType = Object.freeze({
Temperature: "Temperature"
});
const SensorSchema = new mongoose.Schema({
id: { type: Number, required: true },
type: { type: Object.values(SensorType), required: true },
});
Object.assign(SensorSchema.statics, { SensorType });
const Sensor = mongoose.model('Sensor', SensorSchema);
const DataEntrySchema = new mongoose.Schema({
datetime: { type: String, required: true },
sensor: { type: SensorSchema, required: true },
value: { type: Number, required: true }
});
const DataEntry = mongoose.model('DataEntry', DataEntrySchema);
Loading the DataEntries like this:
mongoose.connect("mongodb://127.0.0.1:27017/",{
useCreateIndex:true,
useNewUrlParser: true,
useUnifiedTopology: true}
).then(() => {
console.log('Database Successfully Connected')
if(fill_default_data) {
DataEntry.create(
JSON.parse(fs.readFileSync(path.resolve(__dirname, 'test_data.json'), 'utf8'))
);
}
}, error => {
console.log(error)
}
);
However, I am noticing that no Sensor-objects are created inside MongoDB, only DataEntries - why is that? And how can I create Sensor-objects as well?
Of course, a DataEntry object has the sensor attached but if I call Sensor.find().then( sensors => res.json(sensors) ) an empty array is returned.
You probably can't use a schema in another schema. You need to use refs instead.
So something like this sensor: { type: SensorSchema, required: true } won't work.
You should replace it with sensor: { type: number, required: true, ref: 'Sensor' },, where the ref is the name of the model you want to refer to as a string. Notice that the type is a number as you want to pass the id of the relevant SensorDocument in the DataEntryDocument.
Moreover id is a virtual, you should use _id instead when you want to spec out ids in mongoose schemes.
So your mongoose schemes should look like:
const SensorSchema = new mongoose.Schema({
_id: { type: mongoose.Schema.Types.Number, required: true },
type: { type: mongoose.Schema.Types.String, required: true },
});
const Sensor = mongoose.model('Sensor', SensorSchema);
const DataEntrySchema = new mongoose.Schema({
datetime: { type: mongoose.Schema.Types.String, required: true },
sensor: { type: mongoose.Schema.Types.Number, ref: 'Sensor', required: true },
value: { type: mongoose.Schema.Types.Number, required: true }
});
const DataEntry = mongoose.model('DataEntry', DataEntrySchema);
I still don't know why the Object.freeze and Object.assign are here.
Now if you want a DataEntry, you first need to create a Sensor.
const sensor = new Sensor({ _id: 0, type: 'Temperature' })
await sensor.save()
const dataEntry = new DataEntry({ sensor: 0, datetime: 'some timestamp as string', value: 25 })
await dataEntry.save()
I am leaving the validation-specific logic out as it is out of the scope of this query.
You can checkout docs for mongoose populate for more information.

property pre does not exist on schema in mongoose

I am trying to detect changes in the document via pre hook but it typescript is giving me error that this property does not exist.
following Structured style, not OOP
// category.schema.ts
const categorySchema = new Schema({
category_id: { type: Number, required: true, unique: true },
name: { type: String, required: true, unique: true },
icon_url: { type: String, required: true },
items_quantity: { type: Number, required: true },
items: [
item_id: { type: Number, required: true, unique: true },
item_name: { type: String, required: true }
]
})
const Category: Model<Category> = model<Category>('Category', categorySchema);
export default Category;
Now I want to check for document changes on deletion of subdocument.
import CategorySchema from "../schemas/category.schema"; // schema path
router.delete('/:category/:item', async (req, res) => { // removes an item
let itemsQuantity: number;
let category = await CategorySchema.findOneAndUpdate(
{ category_id: req.params.category },
{ $pull: { items: { item_id: req.params.item } } },
{ new: true });
// pre does not exist
CategorySchema.pre('save', function(next) {
if(category.isModified()) {
log('changed');
} else {
log('not changed')
}
})
const data = await category.save();
res.status(200).send(req.params.item);
})
How to get or enable this hook, any suggestions?

Node.js dependson access relationship attribute?

I have two simple modes:
PresentationType:
var keystone = require('keystone');
var PresentationType = new keystone.List('PresentationType', {
autokey: { from: 'name', path: 'key', unique: true },
});
PresentationType.add({
name: { type: String, required: true },
t1: { type: Boolean },
t2: { type: Boolean },
});
PresentationType.relationship({ ref: 'StaticPage', path: 'pages', refPath: 'presentationType' });
PresentationType.register();
Static Page:
var keystone = require('keystone');
var Types = keystone.Field.Types;
var StaticPage = new keystone.List('StaticPage', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
drilldown: 'presentationType',
});
StaticPage.add({
title: { type: String, required: true },
presentationType: { type: Types.Relationship, ref: 'PresentationType', many: false },
text1: { type: String, dependsOn: { presentationType.t1: true } },
text2: { type: String, dependsOn: { presentationType.t2: true } },
});
StaticPage.defaultColumns = 'title';
StaticPage.register();
First i create a presentation type that has boolean attributes, text1 and text2
Secondly when i create a page and specify it's presentation type, i want to be able to display certain fields based on the presentation type boolean.
So far i cant seem to find an answer to it.
The dependsOn attribute cannot be used across a relationship field; that field would constantly need to be populated with that relationship. dependsOn within a model can only be used within other static fields of the same model (and not across different models.)
http://keystonejs.com/docs/database/#fields-conditional

Is there a tag field type in Keystone JS?

I'm looking for a tag field type which will autocomplete if the tag already exists, or simply add the tag if it doesn't. I think there are a lot of implementations of this in other CMS' and I wanted to shake the tree to see if someone had already done this before I roll up my sleeves. Assuming it existed, I imagine it would be implemented as follows:
var keystone = require('keystone'),
Types = keystone.Field.Types;
var Verbiage = new keystone.List('Verbiage', {
autokey: { path: 'slug', from: 'title', unique: true },
map: { name: 'title' },
defaultSort: '-createdAt',
label: "Verbiage",
plural : "Verbiage"
});
Verbiage.add({
title: { type: String, required: true },
author: { type: Types.Relationship, ref: 'User' },
tagged: { type: Types.Tag, required: false, many: true },
createdAt: { type: Date, default: Date.now },
publishedAt: Date
});
Verbiage.register();

Image gallery with caption using CloudinaryImage on keystonejs

I'm using keystonejs and CloudinaryImages to create an Image Gallery.
{ type: Types.CloudinaryImages }
I need the ability to add a caption to the images.
I was also reading this:
https://github.com/keystonejs/keystone/pull/604
but I could not figure out if this option is already in place or not.
Any idea?
Thanks.
I had a similar problem, I wanted to be able to give Images there own descriptions and other attributes, while also being included in a Gallery with a Gallery description.
This may be more than you are looking for but here is a Image model:
var keystone = require('keystone'),
Types = keystone.Field.Types;
/**
* Image Model
* ==================
*/
var Image = new keystone.List('Image', {
map: { name: 'name' },
autokey: { path: 'slug', from: 'name', unique: true }
});
Image.add({
name: { type: String, required: true },
image: { type: Types.CloudinaryImage, autoCleanup: true, required: true, initial: false },
description: { type: Types.Textarea, height: 150 },
});
Image.relationship({ ref: 'Gallery', path: 'heroImage' });
Image.relationship({ ref: 'Gallery', path: 'images' });
Image.register();
And the Galleries that contain these images looks like this:
var keystone = require('keystone'),
Types = keystone.Field.Types;
/**
* Gallery Model
* =============
*/
var Gallery = new keystone.List('Gallery', {
map: { name: 'name' },
autokey: { path: 'slug', from: 'name', unique: true }
});
Gallery.add({
name: { type: String, required: true},
published: {type: Types.Select, options: 'yes, no', default: 'no', index: true, emptyOption: false},
publishedDate: { type: Types.Date, index: true, dependsOn: { published: 'yes' } },
description: { type: Types.Textarea, height: 150 },
heroImage : { type: Types.Relationship, ref: 'Image' },
images : { type: Types.Relationship, ref: 'Image', many: true }
});
Gallery.defaultColumns = 'title, published|20%, publishedDate|20%';
Gallery.register();
You will need to create Template Views and Routes to Handle this, but it isn't too much more work - these are just the Models - let me know if you would like me to post the routes I am using for this, I am using Handlebars for my views so that may not be as helpful.

Resources