Virtual "name" field? - keystonejs

I need to have the name field of a model be virtual, created by concatenating two real fields together. This name is just for display only. I've tried the virtual examples in the doc, no luck. Keystone 4 beta5.
var keystone = require('keystone')
_ = require('underscore');
var Types = keystone.Field.Types;
/**
* Foo Model
* ==================
*/
var Foo = new keystone.List('Foo', {
map: {name: 'fooname'},
track: true
});
Foo.add({
bar: { type: Types.Relationship, required: true, initial: true, label: 'Barref', ref: 'Bar', many: false },
order: { type: Types.Select, required: true, initial: true, label: 'Order', options: _.range(1,100) },
price: { type: Types.Money, format: '$0,0.00', label: 'Price', required: true, initial: true },
});
Foo.schema.virtual('fooname').get(function() {
return this.bar+ ' ' + this.order;
});
Foo.defaultColumns = 'fooname, bar, order, price';
Foo.register();
When I use this model definition, I don't see the virtual name in the defaultcolumns list. I want to make a virtual name so lookups are easier when this model is used as a relationship.

You don't need a virtual to do this. Keystone allows you to track and recalculate a field every time the document is saved. You can enable those options in order to create a function which concatenates these two values for you (either synchronously or asynchronously, your choice.)
One other thing I noticed is that bar is a Relationship, which means you will need to populate that relationship prior to getting any useful information out of it. That also means your value function will have to be asynchronous, which is as simple as passing a callback function as an argument to that function. Keystone does the rest. If you don't need any information from this bar, and you only need the _id (which the model always has), you can do without the keystone.list('Bar') function that I included.
http://keystonejs.com/docs/database/#fields-watching
The map object also refers to an option on your model, so you'll need a fooname attribute on your model in any scenario, though it gets calculated dynamically.
var keystone = require('keystone'),
_ = require('underscore');
var Types = keystone.Field.Types;
/**
* Foo Model
* ==================
*/
var Foo = new keystone.List('Foo', {
map: {name: 'fooname'},
track: true
});
Foo.add({
fooname: { type: Types.Text, watch: true, value: function (cb) {
// Use this if the "bar" that this document refers to has some information that is relevant to the naming of this document.
keystone.list('Bar').model.findOne({_id: this.bar.toString()}).exec(function (err, result) {
if (!err && result) {
// Result now has all the information of the current "bar"
// If you just need the _id of the "bar", and don't need any information from it, uncomment the code underneath the closure of the "keystone.list('Bar')" function.
return cb(this.bar.name + " " + this.order);
}
});
// Use this if you don't need anything out of the "bar" that this document refers to, just its _id.
// return cb(this.bar.toString() + " " + this.order);
} },
bar: { type: Types.Relationship, required: true, initial: true, label: 'Barref', ref: 'Bar', many: false },
order: { type: Types.Select, required: true, initial: true, label: 'Order', options: _.range(1,100) },
price: { type: Types.Money, format: '$0,0.00', label: 'Price', required: true, initial: true },
});
Foo.defaultColumns = 'fooname, bar, order, price';
Foo.register();

try this:
Foo.schema.pre('save', function (next) {
this.name = this.bar+ ' '+ this.order;
next();
});

Could you provide more information? What is currently working? How should it work?
Sample Code?
EDIT:
After creating the model Foo, you can access the Mongoose schema using the attribute Foo.schema. (Keystone Concepts)
This schema provides a pre-hook for all methods, which registered hooks. (Mongoose API Schema#pre)
One of those methods is save, which can be used like this:
Foo.schema.pre('save', function(next){
console.log('pre-save');
next();
});

Related

How to change the columns collection set of a kendo TreeList dynamically?

Try to change the columns list dynamically via a query ...
When I construct the TreeList, I call for columns :
$("#treelist").kendoTreeList({
columns: AnalyseCenterSKUService.getKPIColumnList($scope)
If I return a simple array with the fields, it's working ..
If I call a $http.get (inside my getKPIColumnList(..) function) which add some columns to the existing array of columns, the TreeList is not constructed correctly.
Any suggestion will be really appreciated ! :)
EDIT 22-10-2019 09:00
Treelist init
$("#treelist").kendoTreeList({
columns: AnalyseCenterSKUService.getKPIColumnList($scope),
scrollable: true,
columnMenu : {
columns : true
},
height: "100%",
dataBound: function (e) {
ExpandAll();
},
dataSource: {
schema: {
model: {
id: "id",
parentId: "parentId",
fields: {
id: { type: "number" },
parentId: { type: "number", nullable: true },
fields: {
id: { type: "number" },
parentId: { type: "number", nullable: false }
}
}
}
},
transport: {
read: {
url: "/api/AnalyseCenter/GetWorkOrderTree/0",
dataType: "json"
}
}
}
The getKPIColumnList return an static array + some push with dynamic columns (from DB)
angular.module('AnalyseCenterDirectives')
.service ('AnalyseCenterSKUService', function ($http) {
var toReturn = [ {field: "Name", title: "HiƩrachie SKU", width: "30%" }, ..., ..., .... ];
I try in this function to push DB result
return $http.get("/api/AnalyseCenter/GetWorkOrderHistorianAdditonalColumns?equipmentName=" + equipmentName)
.then(function (result) {
var data = result.data;
if (data && data !== 'undefined') {
var fromDB = data;
angular.forEach(fromDB, function (tag) {
var tagName = tag.replace(".","_");
toReturn.push({
field: tagName, title: tag, width: '10%',
attributes: { style: "text-align:right;"} })
})
The stored procedure GetWorkOrderHistorianAdditonalColumns returns a list of string (future column)
That is because ajax is async, that means your tree list is being initialized before the request finishes. A classic question for JavaScript newcomers. I suggest you take a while to read about ajax, like How does AJAX works for instance.
Back to your problem. You need to create your tree list inside the success callback(I can't give you a more complete solution since I don't know what you're doing inside your function or which framework you're using to open that ajax request) with the result data, which is probably your columns. Then it would work as if you're initializing it with arrays.

How to add pre-hook in keystonejs?

I want to add multiple select options field. But the docs state that doesn't allow for multiple select. But recommends pre-hook for that case.
Stores a String or Number in the model. Displayed as a select field in
the Admin UI. Does not allow for multiple items to be selected. If you
want to provide multiple values, you can use TextArray or NumberArray,
although neither will have the same constrained input. You can limit
the options using a pre-save hook.
I search for pre-hook but it seems came from mongoose. And in my case, I create the model using Keystone so that I can use it in admin page
var keystone = require('keystone');
var Types = keystone.Field.Types;
var MyModel = new keystone.List('MyModel');
MyModel.add({
aField: { type: Types.TextArray, required: false, initial: true },
});
so how do I create the pre-hook? for example, I want to limit the TextArray to be set of ('a','b','c')?
I have set up pre-save hooks like this (or something similar to this. Did not test this code).
var keystone = require('keystone');
var Types = keystone.Field.Types;
/**
* Musician Model
* ==========
*/
var Musician = new keystone.List('Musician', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
});
Musician.add({
title: { type: String, required: true },
published: { type: Types.Boolean, default: false },
musicianId: { type: String, note: noteUpdateId },
});
Musician.schema.pre('save', function (next) {
console.log(this.title);
console.log(this.isNew);
if (this.isNew) {
// generates a random ID when the item is created
this.musicianId = Math.random().toString(36).slice(-8);
}
next();
});
Musician.defaultColumns = 'title, published, musicianId';
Musician.register();

Dealing with concurrent rally sessions in Custom html app

I have a rally poker app which is custom html where there is a screen for Team member and a screen for Scrum Master. What I need is how to control concurrent updates from team members when they use this custom html app. As of today, the app only takes the latest updates from the user who submitted last in concurrent session. It overrides everyone else data. It would be great help if someone can post the sample code of how to check for concurrent session and stop overriding of user inputs in concurrent sessions. Thanks for your help.
Click here for Scrum Master View Screenshot
#kyle - I tried to add Version ID as mentioned by you
Thanks Kyle. I did tried a cumbersome way of checking the versionID by repeating the fetch code however in my testing, it proved that versionID wasnt setting properly for concurrent sessions. I have pasted the code snippet which has the versionID check. Can you please review and let me know if anything needs to be added. Appreciate your help
Ext.create('Ext.window.Window', {
title: 'Planning Poker-Team Member view UserStoryNumber : '+userstorynumber, height: 200, width: 400, layout: 'fit',
items: [
//*************** START PANEL FORM 1 *******************
Ext.create('Ext.form.Panel', {
bodyPadding: 5, width: 350, url: 'save-form.php',
layout: { type: 'vbox', align: 'stretch', pack: 'start' },
defaults: { anchor: '100%' },
//Fields
defaultType: 'textfield',
items: [
{ fieldLabel: 'Estimate', name: 'Estimate', allowBlank: false, id: 'estimate' },
{ fieldLabel: 'Justification', name: 'Justification', allowBlank: false, id: 'justification' }
],
// Reset and Submit buttons
buttons: [
//{ text: 'Reset', handler: function () { this.up('form').getForm().reset(); } },
{
text: 'Submit', formBind: true, disabled: true,
handler: function () {
//alert(f_newEstimate('name','points','role','justification'));
var EstimateObject = getExistingEstimates(result);
if (EstimateObject)
console.log('Notes ************ ' + result.get('Notes'));
console.log('Stringfy ************ ' + JSON.stringify(EstimateObject));
//rec.set('PlanEstimate', estFinal);
var jsonobj = f_newEstimate(name, Ext.getCmp('estimate').getValue(), role, Ext.getCmp('justification').getValue());
console.log("json raw is", jsonobj);
//console.log("json justi is", jsonobj.justification);
console.log("PO note is", result.get('c_PONote'));
//var existingPONote = result.get('c_PONote');
//var newponote = existingPONote + ',' + jsonobj;
var CurrentVersionID;
var newVersionID;
//Exp Karthik Starts
var myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'User Story',
autoLoad: true, // <----- Don't forget to set this to true! heh
filters: [
{
property: 'ObjectID',
operator: '=',
value: objectId
}
],
listeners: {
load: function (myStore, myData, success) {
CurrentVersionID = myStore.data.items["0"].data.VersionId;
console.log("Version ID is", myStore.data.items["0"].data.VersionId);
},
scope: this // This tells the wsapi data store to forward pass along the app-level context into ALL listener functions
},
fetch: ['ObjectID', 'c_PONote', 'VersionId'] // Look in the WSAPI docs online to see all fields available!
});
var existingPONote = result.get('c_PONote');
var newponote = '';
if (existingPONote.length == 0) {
newponote = '[' + jsonobj + ']';
}
else {
replacestr = ',' + jsonobj + ']';
newponote = existingPONote.replace(']', replacestr);
}
rec.set('c_PONote', newponote);
var myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'User Story',
autoLoad: true, // <----- Don't forget to set this to true! heh
filters: [
{
property: 'ObjectID',
operator: '=',
value: objectId
}
],
listeners: {
load: function (myStore, myData, success) {
newVersionID = myStore.data.items["0"].data.VersionId;
console.log("Version ID is", myStore.data.items["0"].data.VersionId);
},
scope: this // This tells the wsapi data store to forward pass along the app-level context into ALL listener functions
},
fetch: ['ObjectID', 'c_PONote', 'VersionId'] // Look in the WSAPI docs online to see all fields available!
});
if (CurrentVersionID == newVersionID) {
rec.save();
}
else
{
var myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'User Story',
autoLoad: true, // <----- Don't forget to set this to true! heh
filters: [
{
property: 'ObjectID',
operator: '=',
value: objectId
}
],
listeners: {
load: function (myStore, myData, success) {
newVersionID = myStore.data.items["0"].data.VersionId;
console.log("Version ID is", myStore.data.items["0"].data.VersionId);
},
scope: this // This tells the wsapi data store to forward pass along the app-level context into ALL listener functions
},
fetch: ['ObjectID', 'c_PONote', 'VersionId'] // Look in the WSAPI docs online to see all fields available!
});
var existingPONote = myStore.data.items["0"].data.c_PONote;
var newponote = '';
if (existingPONote.length == 0) {
newponote = '[' + jsonobj + ']';
}
else {
replacestr = ',' + jsonobj + ']';
newponote = existingPONote.replace(']', replacestr);
}
rec.set('c_PONote', newponote);
console.log("Concurrent versions saved", newponote);
rec.save();
}
console.log('Submit Success1');
this.up('window').close();
}
}]
All objects in WSAPI include a VersionId field which is automatically incremented on the server every time the object is updated. The way the product deals with concurrency issues is to always fetch this VersionId field. Then users can make updates locally. When users save, first the story is read again. If the VersionId is the same as it was when it was first read, the update is safe to send. If that VersionId is higher though, that means another user has updated the story in the meantime.
In the case of a conflict you can just merge your local changes into that newly read object and then try the save again. You can repeat this as needed until the user is able to perform a clean save.

Field (Test.title) is required but not initial, and has no default or generated value

I was wondering if someone knows why I get that error, my model
var Test = new keystone.List('Test', {
autokey: { from: 'title', path: 'key', unique: true }
});
Test.add({
title: { type: String, required: true },
All I did was change the values from the post example below
var Test = new keystone.List('Test', {
autokey: { from: 'name', path: 'key', unique: true }
});
Test.add({
name: { type: String, required: true },
I can;t understand why it works with name and not with title
use map key inside option list
var Test = new keystone.List('Test', {
map: { name: 'title' },
autokey: { from: 'title', path: 'key', unique: true }
});
refs: http://keystonejs.com/docs/database/#lists-options
An object that maps fields to special list paths. Each path defaults
to its key if a field with that key is added. Mappable paths include
name - the field that contains the name of the item, for display in
the Admin UI

Manually adding columns to rallycardboard component

I am creating a rallycardboard where each column represents a release, and the cards are Features to be scheduled into those releases. The default mechanics of the component render all available releases as columns on the board. For our particular application this is unreasonable, since there are thousands of Releases in our workspace.
I was able to overwrite the addColumn method to only include a column if it is a Release which at least one Feature from the group is assigned to. The next step is to make it so that a user can manually add a Release column that doesn't currently have any assigned work. To do this, I stored all the excluded columns from the first step and created a combo box with those values. I would like it so that when the user selects a Release from the combo box, that Release column is added to the board.
I was able to reconfigure my addColumn method to allow for manual override (as apposed to trying to match with an existing Feature's Release). I verified that the column was added to the boards columns by calling board.getColumns() and the configurations look the same for both the existing and added columns. However, I get an error message when calling board.renderColumns() which appears to be the result of trying to write to a container that doesn't yet exist (the column isn't created yet).
Maybe I'm going about this the wrong way. Is there another way I can more easily decide which columns to include, and which to exclude, on the rallycardboard component?
Here is an example where the board columns are based on releases that have features scheduled. To add a columns for releases that have no features currently scheduled select releases from the mulitpicker.
The app is available in this github repo.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
_releasesWithFeatures: [],
_uniqueColumns: [],
_additionalColumns: [],
_updatedColumns: [],
_cardBoard: null,
launch: function() {
var that = this;
this._releasePicker = Ext.create('Rally.ui.picker.MultiObjectPicker', {
fieldLabel: 'Choose a Release',
modelType: 'Release'
});
this.add(this._releasePicker);
this.add({
xtype: 'rallybutton',
id: 'getReleases',
text: 'Add Selected Releases',
handler: function(){
that._getSelectedReleases();
}
})
Ext.create('Rally.data.WsapiDataStore', {
model: 'PortfolioItem/Feature',
fetch: ['FormattedID','Name','Release'],
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'Release',
operator: '!=',
value: null
}
],
listeners: {
load: this._onScheduledFeaturesLoaded,
scope: this
}
});
},
_onScheduledFeaturesLoaded: function(store, data){
var that = this;
if (data.length !==0) {
_.each(data, function(feature){
console.log('feature ', feature.get('FormattedID'), 'scheduled for ', feature.get('Release')._refObjectName, feature.get('Release')._ref);
that._releasesWithFeatures.push(feature.get('Release'))
});
that._makeBoard();
}
else{
console.log('there are no features scheduled for a release')
}
},
_makeBoard: function(){
if (this._cardBoard) {
this._cardBoard.destroy();
}
var columns = [];
_.each(this._releasesWithFeatures, function(rel){
columns.push({value: rel._ref, columnHeaderConfig: {headerTpl: '{release}', headerData: {release: rel._refObjectName}}});
});
this._uniqueColumns = _.uniq(columns, 'value');
var cardBoard = {
xtype: 'rallycardboard',
itemId: 'piboard',
types: ['PortfolioItem/Feature'],
attribute: 'Release',
fieldToDisplay: 'Release',
columns: this._uniqueColumns
};
this._cardBoard = this.add(cardBoard);
},
_getSelectedReleases: function(){
var that = this;
var expandedColumns = [];
var selectedReleases = this._releasePicker._getRecordValue();
console.log(selectedReleases.length);
if (selectedReleases.length > 0) {
_.each(selectedReleases, function(rel) {
var releaseName = rel.get('Name');
var releaseRef = rel.get('_ref');
that._additionalColumns.push({value: releaseRef, columnHeaderConfig: {headerTpl: '{release}', headerData: {release: releaseName}}});
});
}
expandedColumns = _.union(that._uniqueColumns, that._additionalColumns);
this._updatedColumns = _.uniq(expandedColumns, 'value');
this._updateBoard();
},
_updateBoard: function(){
var that = this;
if (this._cardBoard) {
this._cardBoard.destroy();
}
var cardBoard = {
xtype: 'rallycardboard',
types: ['PortfolioItem/Feature'],
attribute: 'Release',
fieldToDisplay: 'Release',
columns: that._updatedColumns,
};
this._cardBoard = this.add(cardBoard);
}
});