How to populate custom React component in Keystone Admin - node.js

We're using Keystone (version 4.0.0-beta.8) as our Node.js CMS for the first time for a project and we're really liking it. However we're running into a vital missing feature of the CMS and that is adding custom components to the admin views.
What we would like to achieve is to add a custom React component to one of the Keystone admin screens and populate it with data from a field watch function
This component should only be visible for one of the list models. Right now, the only way to do this, is to edit one of the core files:
/node_modules/keystone/admin/client/App/screens/Item/index.js
and add some conditional rendering around it:
{(this.props.params.listId === 'my-list-model') ? (
<MyComponent>{this.props.params.listId}</MyComponent>
) : null }
This works, but is not ideal off-course since you're overwriting core files. But I could overcome this short-come if I knew how I can feed this custom component with data from the Keystone list model declaration:
In: /server/models/MyListModel.js
import keystone from 'keystone';
import { someFunction } from './myFunctions';
var MyListModel = new keystone.List('MyListModel', {
map: { name: 'title' },
});
MyListModel.add({
title: { type: String, required: true },
data: { type: Types.Code, language: 'json', watch: 'title', value: watchTitle, noedit: true },
});
MyListModel.register();
function watchTitle(callback) {
if (this.title) {
function cb(error, result) {
if (result) {
// Send result to React Component in Admin screen
}
callback(error, result);
}
someFunction(this.title, cb);
} else {
callback(null, '');
}
}
Does anyone bump into the same issue or have any clue how to send data to a custom component in the react admin view of Keystone?
Thanks a lot!

Related

How to implement a selector in easy: search for Meteor, using React instead of Blaze

I'm trying to follow the documentation and examples to add a server-side selector to a search function in my Meteor app, implemented using the Easy Search plugin. The end goal is to ensure that only documents the user has permission to see are returned by searching.
I can see a selector working in the Leaderboard example, but I can't get it to work in my code.
Versions:
Meteor 1.7.0.1
easy:search#2.2.1
easysearch:components#2.2.2
easysearch:core#2.2.2
I modified the Meteor 'todos' example app to demonstrate the problem, and my demo code is in a repo.
NOTE! to demonstrate the problem, you need to create an account in the demo app, then create a list and make it private. This add the 'userId' field to the list.
Then you can search for the name of the list, by typing in the search box near the top of the main section; search results are written to the browser console.
The first problem is that if I copy the code from the example in the documentation, I see a server error 'searchObject is not defined:
copied from docs, causes an error: imports/api/lists/lists.js
export const MyIndex = new Index({
'collection': Lists,
'fields': ['name'],
engine: new MongoDBEngine({
selector(searchDefinition, options, aggregation) {
// retrieve the default selector
const selector = this.defaultConfiguration()
.selector(searchObject, options, aggregation)
// options.search.userId contains the userId of the logged in user
selector.userId = options.search.userId
return selector
},
}),
});
It seems there is an error in the docs.
Working instead from the leaderboard example, the code below runs but intermittently returns no results. For example if I have a list called "My list", and I type the search term 's', sometimes the list is returned from the search and sometimes it is not. If I use the MiniMongo engine it all works perfectly.
index selector {"$or":[{"name":{"$regex":".*my.*","$options":"i"}}],"userId":"Wtrr5FRHhkKuAcrLZ"}
client and server: imports/api/lists/lists.js
export const MyIndex = new Index({
'collection': Lists,
'fields': ['name'],
'engine': new MongoDBEngine({
selector: function (searchObject, options, aggregation) {
let selector = this.defaultConfiguration().selector(searchObject, options, aggregation);
selector.userId = options.search.userId;
console.log('index selector', JSON.stringify(selector));
return selector;
}
}),
permission: () => {
return true;
},
});
client: imports/ui/components/lists-show.js
Template.Lists_show.events({
'keyup #search'(event) {
console.log('search for ', event.target.value);
const cursor = MyIndex.search(event.target.value);
console.log('count',cursor.count());
console.log('results', cursor.fetch());
},
});
client: imports/ui/components/lists-show.html
<input id="search" type="text" placeholder="search..." />
Edit: I think the problem is that while the Minimongo engine runs on the client, the MongoDBEngine runs on the server and there are timing issues with the results. The docs show using Tracker.autorun, but that's not a natural fit with my React / Redux app. I'll post an answer if I manage to figure something out - I can't be the only person trying to do something like this.
I got it working in my React / Redux / Meteor app. Things to note:
The cursor MyIndex.search(searchTerm) is a reactive data source - you can't just use it as a return value. When searching on the client with MiniMongo this isn't an issue, but it's important when you use MongoDBEngine to search on the server, because it's asynchronous. In React you can wrap the cursor in withTracker to pass data to the component reactively. In Blaze you would use autorun.tracker. This is shown in the docs but not explained, and it took me a while to understand what was happening.
The docs have an error in the selector example, easily corrected but it's confusing if you have other problems in your code.
With MongoDBEngine, 'permission' must be specified - it does not default to 'true'. Without it, you will see no results.
Writing out the default selector object to the console let me see how it's constructed, and then create a new selector that returns MyDocs that are either public or created by the user.
My code is below. In case it helps anybody else, I've shown how to search on tags also, which are objects with a name property stored in a collection Tags. Each MyDoc has a 'tags' property which is an array of tag ids. The selector first searches the Tags collection to find tags whose name matches the search term, then selects docs in MyDocs with the ids of those tags in their doc.tags array.
There may be a better way to find the search term, or to structure the Tags search, but this is what I could get working.
On server and client:
import { Index, MongoDBEngine } from 'meteor/easy:search';
export const MyDocs = new Mongo.Collection('mydocs');
export const Tags = new Mongo.Collection('tags');
export const MyIndex = new Index({
'collection': MyDocs,
'fields': ['name'],
'engine': new MongoDBEngine({
'selector': function (searchObject, options, aggregation) {
const selector = this.defaultConfiguration().selector(searchObject, options, aggregation);
console.log('default selector', selector); // this searches on name only
// find docs by tag as well as by name
const searchTerm = searchObject.name;
const matchingTags = Tags.find({ 'name': { '$regex': searchTerm } }).fetch();
const matchingTagIds = matchingTags.map((tag) => tag._id);
selector.$or.push({ 'tags': { '$in': matchingTagIds } });
const newSelector = {
'$and': [
{
'$or': [
{ 'isPublic': { '$eq': true } },
{ 'createdBy': options.search.userId },
],
},
{
'$or': selector.$or,
},
],
};
return newSelector;
},
'fields': (searchObject, options) => ({
'_id': 1,
'createdBy': 1,
'name': 1,
}),
'sort': () => ({ 'name': 1 }),
}),
'permission': () => true,
});
React component in client only code:
import React from 'react';
import { connect } from 'react-redux';
import { withTracker } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types';
import store from '../modules/store';
import {
getSearchTerm,
searchStart,
} from '../modules/search'; // contains Redux actions and partial store for search
import { MyIndex } from '../../modules/collection';
function Search(props) {
// functional React component that contains the search box
...
const onChange = (value) => {
clearTimeout(global.searchTimeout);
if (value.length >= 2) {
// user has entered a search term
// of at least 2 characters
// wait until they stop typing
global.searchTimeout = setTimeout(() => {
dispatch(searchStart(value)); // Redux action which sets the searchTerm in Redux state
}, 500);
}
};
...
// the component returns html which calls onChange when the user types in the search input
// and a list which displays the search results, accessed in props.searchResults
}
const Tracker = withTracker(({ dispatch }) => {
// searchTerm is saved in Redux state.
const state = store.getState();
const searchTerm = getSearchTerm(state); // Redux function to get searchTerm out of Redux state
let results = [];
if (searchTerm) {
const cursor = MyIndex.search(searchTerm); // search is a reactive data source
results = cursor.fetch();
console.log('*** cursor count', cursor.count());
return {
'searchResults': results,
};
})(Search);
export default connect()(Tracker);

Node.js Testing with Mongoose. unique gets ignored

I'm having a little trouble with an integration test for my mongoose application. The problem is, that my unique setting gets constantly ignored. The Schema looks more or less like this (so no fancy stuff in there)
const RealmSchema:Schema = new mongoose.Schema({
Title : {
type : String,
required : true,
unique : true
},
SchemaVersion : {
type : String,
default : SchemaVersion,
enum: [ SchemaVersion ]
}
}, {
timestamps : {
createdAt : "Created",
updatedAt : "Updated"
}
});
It looks like basically all the rules set in the schema are beeing ignored. I can pass in a Number/Boolean where string was required. The only thing that is working is fields that have not been declared in the schema won't be saved to the db
First probable cause:
I have the feeling, that it might have to do with the way I test. I have multiple integration tests. After each one my database gets dropped (so I have the same condition for every test and precondition the database in that test).
Is is possible that the reason is my indices beeing droped with the database and not beeing reinitiated when the next text creates database and collection again? And if this is the case, how could I make sure that after every test I get an empty database that still respects all my schema settings?
Second probable cause:
I'm using TypeScript in this project. Maybe there is something wrong in defining the Schema and the Model. This is what i do.
1. Create the Schema (code from above)
2. Create an Interface for the model (where IRealmM extends the Interface for the use in mongoose)
import { SpecificAttributeSelect } from "../classes/class.specificAttribute.Select";
import { SpecificAttributeText } from "../classes/class.specificAttribute.Text";
import { Document } from "mongoose";
interface IRealm{
Title : String;
Attributes : (SpecificAttributeSelect | SpecificAttributeText)[];
}
interface IRealmM extends IRealm, Document {
}
export { IRealm, IRealmM }
3. Create the model
import { RealmSchema } from '../schemas/schema.Realm';
import { Model } from 'mongoose';
import { IRealmM } from '../interfaces/interface.realm';
// Apply Authentication Plugin and create Model
const RealmModel:Model<IRealmM> = mongoose.model('realm', RealmSchema);
// Export the Model
export { RealmModel }
Unique options is not a validator. Check out this link from Mongoose docs.
OK i finally figured it out. The key issue is described here
Mongoose Unique index not working!
Solstice333 states in his answer that ensureIndex is deprecated (a warning I have been getting for some time now, I thought it was still working though)
After adding .createIndexes() to the model leaving me with the following code it works (at least as far as I'm not testing. More on that after the code)
// Apply Authentication Plugin and create Model
const RealmModel:Model<IRealmM> = mongoose.model('realm', RealmSchema);
RealmModel.createIndexes();
Now the problem with this will be that the indexes are beeing set when you're connection is first established, but not if you drop the database in your process (which at least for me occurs after every integration test)
So in my tests the resetDatabase function will look like this to make sure all the indexes are set
const resetDatabase = done => {
if(mongoose.connection.readyState === 1){
mongoose.connection.db.dropDatabase( async () => {
await resetIndexes(mongoose.models);
done();
});
} else {
mongoose.connection.once('open', () => {
mongoose.connection.db.dropDatabase( async () => {
await resetIndexes(mongoose.models);
done();
});
});
}
};
const resetIndexes = async (Models:Object) => {
let indexesReset: any[] = [];
for(let key in Models){
indexesReset.push(Models[key].createIndexes());
}
Promise.all(indexesReset).then( () => {
return true;
});
}

Meteor: sortablejs method not being called

I've got a component that when simplified looks like so:
import Sortable from 'react-sortablejs';
class App extends Component {
constructor(props) {
super(props);
}
updateItemOrder() {
let items = this.props.items;
items.forEach((item, index) => {
console.log(item._id);
console.log(index);
updateItem.call({ _id: item._id, update: { priority: index } });
});
};
render() {
<Sortable
tag='ul'
className='item-list list-group'
onChange={ this.updateItemOrder() }>
{ this.renderItems() }
</Sortable>
}
}
My console lists the item._id and the index on page load, but nothing happens when I drag-and-drop items in my sortable list. Changing to a <ReactSortable> component doesn't help, either (as per the docs).
From your above code specifically this part here,
updateItemOrder() {
let items = this.props.items;
items.forEach((item, index) => {
console.log(item._id);
console.log(index);
updateItem.call({ _id: item._id, update: { priority: index } });
});
};
It would seem that you are trying to mutate props which is a no go in react. To cause a re-render in react, you need to update state and tell react this change is state should cause a re-render. This is done using setState. There 2 options that come to mind right of the bat. First you can capture your props in the local state of this component and update that, or you can call a method back in your parent component to update its state forcing it to re-render.

Sails.js - Get an object (model) using multiple join

I am new to node.js and newer to Sails.js framework.
I am currently trying to work with my database, I don't understand all the things with Sails.js but I manage to do what I want step by step. (I am used to some PHP MVC frameworks so it is not too difficult to understand the structure.)
Here I am trying to get a row from my database, using 2 JOIN clause. I managed to do this using SQL and the Model.query() function, but I would like to do this in a "cleaner" way.
So I have 3 tables in my database: meta, lang and meta_lang. It's quite simple and a picture being better than words, here are some screenshots.
meta
lang
meta_lang
What I want to do is to get the row in meta_table that match with 'default' meta and 'en' lang (for example).
Here are Meta and Lang models (I created them with sails generate model command and edited them with what I needed):
Meta
module.exports = {
attributes: {
code : { type: 'string' },
metaLangs:{
collection: 'MetaLang',
via: 'meta'
}
}
};
Lang
module.exports = {
attributes: {
code : { type: 'string' },
metaLangs:{
collection: 'MetaLang',
via: 'lang'
}
}
};
And here is my MetaLang model, with 3 functions I created to test several methods. The first function, findCurrent, works perfectly, but as you can see I had to write SQL. That is what I want to avoid if it is possible, I find it more clean (and I would like to use Sails.js tools as often as I can).
module.exports = {
tableName: 'meta_lang',
attributes: {
title : { type: 'string' },
description : { type: 'text' },
keywords : { type: 'string' },
meta:{
model:'Meta',
columnName: 'meta_id'
},
lang:{
model:'Lang',
columnName: 'lang_id'
}
},
findCurrent: function (metaCode, langCode) {
var query = 'SELECT ml.* FROM meta_lang ml INNER JOIN meta m ON m.id = ml.meta_id INNER JOIN lang l ON l.id = ml.lang_id WHERE m.code = ? AND l.code = ?';
MetaLang.query(query, [metaCode, langCode], function(err, metaLang) {
console.log('findCurrent');
if (err) return console.log(err);
console.log(metaLang);
// OK this works exactly as I want (I would have prefered a 'findOne' result, only 1 object instead of an array with 1 object in it, but I can do with it.)
});
},
findCurrentTest: function (metaCode, langCode) {
Meta.findByCode(metaCode).populate('metaLangs').exec(function(err, metaLang) {
console.log('findCurrentTest');
if (err) return console.log(err);
console.log(metaLang);
// I get what I expected (though not what I want): my meta + all metaLangs related to meta with code "default".
// What I want is to get ONE metaLang related to meta with code "default" AND lang with code "en".
});
},
findCurrentOthertest: function (metaCode, langCode) {
MetaLang.find().populate('meta', {where: {code:metaCode}}).populate('lang', {where: {code:langCode}}).exec(function(err, metaLang) {
console.log('findCurrentOthertest');
if (err) return console.log(err);
console.log(metaLang);
// Doesn't work as I wanted: it gets ALL the metaLang rows.
});
}
};
I also tried to first get my Meta by code, then my Lang by code, and MetaLang using Meta.id and Lang.id . But I would like to avoid 3 queries when I can have only one.
What I'm looking for would be something like MetaLang.find({meta.code:"default", lang.code:"en"}).
Hope you've got all needed details, just comment and ask for more if you don't.
Do you know what populate is for ? its for including the whole associated object when loading it from the database. Its practically the join you are trying to do, if all you need is row retrieval than quering the table without populate will make both functions you built work.
To me it looks like you are re-writing how Sails did the association. Id suggest giving the Associations docs another read in Sails documentation: Associations. As depending on your case you are just trying a one-to-many association with each table, you could avoid a middle table in my guess, but to decide better id need to understand your use-case.
When I saw the mySQL code it seemed to me you are still thinking in MySQL and PHP which takes time to convert from :) forcing the joins and middle tables yourself, redoing a lot of the stuff sails automated for you. I redone your example on 'disk' adapter and it worked perfectly. The whole point of WaterlineORM is to abstract the layer of going down to SQL unless absolutely necessary. Here is what I would do for your example, first without SQL just on a disk adapter id create the models :
// Lang.js
attributes: {
id :{ type: "Integer" , autoIncrement : true, primaryKey: true },
code :"string"
}
you see what i did redundantly here ? I did not really need the Id part as Sails does it for me. Just an example.
// Meta.js
attributes: {
code :"string"
}
better :) ?
// MetaLang.js
attributes:
{
title : "string",
desc : "string",
meta_id :
{
model : "meta",
},
lang_id :
{
model : "lang",
}
}
Now after simply creating the same values as your example i run sails console type :
MetaLang.find({meta_id : 1 ,lang_id:2}).exec(function(er,res){
console.log(res);
});
Output >>>
sails> [ { meta_id: 1,
lang_id: 2,
title: 'My blog',
id: 2 } ]
Now if you want to display what is meta with id 1 and what is lang with id 2, we use populate, but the referencing for join/search is just as simple as this.
sails> Meta_lang.find({meta_id : 1 ,lang_id:2}).populate('lang_id').populate('meta_id').exec(function(er,res){ console.log(res); });
undefined
sails> [ {
meta_id:
{ code: 'default',
id: 1 },
lang_id:
{ code: 'En',
id: 2 },
title: 'My blog',
id: 2 } ]
At this point, id switch adapters to MySQL and then create the MySQL tables with the same column names as above. Create the FK_constraints and voila.
Another strict policy you can add is to set up the 'via' and dominance on each model. you can read more about that in the Association documentation and it depends on the nature of association (many-to-many etc.)
To get the same result without knowing the Ids before-hand :
sails> Meta.findOne({code : "default"}).exec(function(err,needed_meta){
..... Lang.findOne({code : "En"}).exec(function(err_lang,needed_lang){
....... Meta_lang.find({meta_id : needed_meta.id , lang_id : needed_lang.id}).exec(function(err_all,result){
......... console.log(result);});
....... });
..... });
undefined
sails> [ { meta_id: 1,
lang_id: 2,
title: 'My blog',
id: 2 } ]
Have you tried:
findCurrentTest: function (metaCode, langCode) {
Meta.findByCode(metaCode).populate('metaLangs', {where: {code:langCode}}).exec(function(err, metaLang) {
console.log('findCurrentTest');
if (err) return console.log(err);
console.log(metaLang);
});
},

AngularJS and Express - Routing multiple urls

I'm currently trying to write a service with the MEAN stack to handle actions on a game. I've followed boiler plates up to the point where I now need to add custom actions.
I have a game model which I can do the usual, save, update, delete on. But I want to add actions like join/leave game. I have tried this currently, for the game service:
window.angular.module('ngff.services.games', [])
.factory('Games', ['$resource',
function($resource){
return $resource(
'games/:gameId/:actionId',
{
gameId: '#_id',
actionId: '#actionId'
},
{
update: {
method: 'PUT',
params: {
actionId: null
}
},
join: {
method: 'POST',
params: {
actionId: 'join'
}
},
leave: {
method: 'POST',
params: {
actionId: 'leave'
}
}
}
);
}]);
Update works correctly, but my other requests always seems to default post to games/:gamesId without adding the actionId on the end. How can I change the location that my form posts to?
I already have the backend worked out, and manually posting to my apis works fine.
I had all this working without angular, but I've never used it before and I'm a little stuck.
Thanks
Ryan

Resources