What is the proper way of adding a custom field to Keystone (to be included in an admin UI form)? - keystonejs

I can see the nice explanation for fields, and what they are made of, here: https://github.com/keystonejs/keystone/tree/v4.0.0-beta.5/fields
How do you go about adding a custom field?
Is adding a custom field (versioned in my own project which depends on keystone, or perhaps done generic enough that could be pushed to npm) a matter of importing it during the keystone setup script and somehow mutating the keystone instance or whatever in order that it also loads my field along with the built-in ones?
EDIT:
The use case is in the context of the admin UI (e.g. you have a User keystone model, and you want the User form to have a new custom field whose UI is an arbitrary react component you implement)

The framework does support storage fields like local file, s3, azure, cloudinary images and embedly fields. That might satisfy your file field needs.
Custom Fields
It appears that the keystone wiki has a short tutorial on the keystonejs wiki and at time of writing, custom types aren't supported in the admin UI.
The example code in the wiki includes a validation method for a credit card number, so this might be the type of functionality that you're looking for.
Here's a short example of what a custom type would look like. It's a field that only accepts Jeff or Alexander as a valid value. You would put it in its own myNameType.js file.
var keystone = require('keystone');
var util = require('util');
/*
Custom FieldType Constructor
#extends Field
#api public
*/
function myName(list, path, options) {
// add your options to this
// call super_
this._nativeType = Text;
myName.super_.call(this, list, path, options);
}
/* inherit Field */
util.inherits(myName, keystone.Field);
/* override or add methods */
myName.prototype.validateInput = function(data) {
console.log('validate my name');
var isValid = false;
if (data && (data.toLower() === 'jeff' || data.toLower() === 'alexander')) {
isValid = true;
}
return isValid;
};
Then register your type in the keystonejs startup file:
// Require keystone
var keystone = require('keystone');
// add a new custom Field Type
Object.defineProperty(
keystone.Field.Types,
'MyName',
{
get: function() {
// or whatever your path is
return require('./myName.js');
}
}
);
From there you can use it in a model (remember to set it to hidden because of the lack of admin UI support):
var keystone = require('keystone');
var Types = keystone.Field.Types;
var Person = new keystone.List('Post', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
sortable: 'unshift',
perPage: 5,
track: true,
autocreate: true
});
Person.add({
name: { type: Types.MyName, label: 'My Name', hidden: true },
heightInInches: { type: Types.Number, label: 'Height (inches)' },
});
Person.register();

Related

Cumulocity - Custom widget configuration

I'm writing a chart widget in cumulocity platform.
In widget comes with the platform,
I can select data point after I select device:
But the widget I wrote can only select device, there is no data point option for me to select:
I know there is c8yComponentsProvider that has options for me to select if I want device target or not. Is there a way for me to choose what data point I want?
From angular 2 it has to be done in this way.
In my custom widget project I added to the usedValue secion of app.module.ts as follows.
useValue: {
id: 'acme.text.widget', // 3.
label: 'Text widget',
description: 'Can display a text',
component: WidgetDemo, // 4.
configComponent: WidgetConfigDemo,
data: {
ng1: {
options: {
noDeviceTarget: true
}
}
}
}
You can disable the device selector in the options of the c8yComponentsProvider:
options: {
noDeviceTarget: true
}
And then use the following directive in your widget config html:
<c8y-data-point-list datapoints="data.datapoints"></c8y-data-point-list>
You need to set the data points to choose in the data.datapoints object on the widget configuration controller. Therefore you can search for managed objects with the fragment c8y_DataPoint.
In the document is an example how to do that with the c8yInventory service:
var filters = {fragmentType: 'c8y_DataPoint', withParents: true};
$scope.data = {};
c8yInventory.list(filters).then(function (devices) {
$scope.data.datapoints = [];
_.forEach(devices, function(dp) {
$scope.data.datapoints.push(dp);
});
});
Note that the c8y-data-point-list is a nonofficial directive. If you face any problems or you want a specific look, you might be faster by writing your own directive.

Accessing properties of ember-data through relationship (Not in the template)

I want to stress that this problem only occurs outside of a template, such as when I try to access properties of related objects while in a controller, unit test, etc. Rendering the template seem to get the property well and work as expected.
Here is a simple example in JS Bin with a failing test http://jsbin.com/ihumuk/4/edit which repros my problem. The passing test asserts that the property is accessible and rendered in the template as expected. The failing test shows that I get null when I try to access the property with get. Really nothing fancy here but I don't understand why it's returning null.
Here is the application part of the JS Bin example:
App.ApplicationRoute = Em.Route.extend({
model: function() {
return App.Foo.find();
}
});
App.Store = DS.Store.extend({
adapter: DS.FixtureAdapter.create()
});
App.Foo = DS.Model.extend({
name: DS.attr("string"),
/**
* The subject under test
*/
childName: function() {
return this.get("child.name");
}.property("child.name"),
child: DS.belongsTo("App.Bar")
});
App.Bar = DS.Model.extend({
name: DS.attr("string")
});
App.Foo.FIXTURES = [{
id: 1,
name: "Fred",
child: 3
}, {
id: 2,
name: "Barney",
child: 4
}];
App.Bar.FIXTURES = [{
id: 3,
name: "Pebbles"
}, {
id: 4,
name: "Bam Bam"
}];
This passes.
test("Child name is rendered", function() {
expect(1);
visit("/").then(function() {
ok(find("div:contains(Pebbles)").length);
});
});
This fails.
test("Child name is accessed", function() {
expect(2);
var foo = App.Foo.find(1);
equal(foo.get("childName"), "Pebbles");
equal(foo.get("child.name"), "Pebbles");
});
This has to be something simple/stupid like forgetting a character or something, but I think I've driven myself too far into frustration to think clearly for a while. Thanks in advance for any help.
You need to use the then to know when the data is loaded
asyncTest("Child name is accessed", function() {
expect(2);
// load the data from server
App.Foo.find(1).then(function(foo) {
// the child id is 3, we need to fetch the remaining data
// and this is async, because of the ajax request
foo.get("child").then(function(child) {
equal(child.get("name"), "Pebbles");
// childName call child.name, but since the
// data is loaded, isn't necessary to use a second then
equal(foo.get("childName"), "Pebbles");
start();
});
});
});
In ember data, like major of the orm's, the data is lazy loaded, for relationships. This is because, isn't needed to return all loaded object graph, let's leave the user ask for what it want, and then load.
Because some implementations are async, like: websql, indexeddb, ajax, websockets etc. The interface of ember-data is async, so you need to use the then method to know when the data is loaded or failed.
The things work in your template, because it are binding aware. Even when the change are async, it will be finished later, and the bindings will be notified and updated.
I have updated your demo, and the tests pass http://jsbin.com/eqojaj/1/edit

Profile feature in Sencha Touch 2 causes problems in production mode build

I have created a Sencha Touch 2 app and built a production mode version. However, I have encountered a big issue with the production build and it running in Phone/Tablet modes.
The current profile implementation of ST2 seems flawed as even if you have a specific profile activated, all views are still loaded in. In my application I want to be able to specify views using the xtype alias in the view config, and have the correct view for phone or tablet profile loaded in without any special coding. If all views from profiles are loaded in then this can't work (one view will always override another).
The only way I could achieve this was to dynamically add the profile at bootup stage (within app.js) like so:
Ext.application({
name: 'MyTestApp',
var activeProfile = Ext.os.is.Phone ? ['Phone'] : ['Tablet'];
requires: [ ... ],
profiles: activeProfile
});
This has worked fine. It means I can then load the correct view and still just use the xtype alias within the config of another view and/or ref in a controller. However, I noticed that when I generate a production build and load up a console window, both of the following are defined:
MyTestApp.views.phone.Login
MyTestApp.views.tablet.Login
Normally the tablet or phone version would be undefined depending on the profile. I'm assuming this is the case because the production mode build has parsed ALL dependencies and then included all views regardless of the profile.
So in my start-up controller I have a button handler which then creates a login view from the xtype.
Controller:
refs: {
loginView: {
selector: 'loginview',
xtype: 'loginview',
autoCreate: true
}
}
Handler:
var loginView = this.getLoginView();
In development mode, the loginView variable will either be MyTestApp.views.tablet.Login or MyTestApp.views.phone.Login depending on the profile.
How do I ensure that the loginview instantiated here gets the correct version depending on the profile when in production mode?
I had been struggling with this, when I would move either of the solutions to the devices, I would be stuck with the fact that all views are referenced and would get some xtype collision always giving me the phone view. ( i had to move to aliases eventually - not sure why :( ). I finally managed to crack this for my use case, just sharing for future reference.
I am running touch 2.3.1 and cordova 3.3.1 with the latest cmd 4.0.2.67
I use the solution from Christopher except I had to change the source code in the sencha touch source directory rather than keep it in the app.js [truthfully I don't know why it hangs when I leave it as an override]
In addition I have had to configure the views the following way in order for:
define a base class for the view with an alias so the controller to understand the ref as it loads first
dynamically assign the alias to the view instantiated by the profile
strip out (using Christopher code)
Base class for the views
Ext.define('MyApp.view.CatalogView', {
extend: 'Ext.Container',
alias: 'widget.catalogview'
});
Assign an alias to the profile specific view
Ext.define('MyApp.profile.Phone', {
extend: 'Ext.app.Profile',
config: {
name: 'Phone',
views: ['CatalogView'],
},
isActive: function() {
return Ext.os.is('Phone');
},
launch: function() {
Ext.ClassManager.setAlias('MyApp.view.phone.CatalogView', 'widget.catalogview');
}
});
Repeat for the tablet view
For all who want to know how I resolved this, I'm now left bald after pulling all my hair out;)
All my profile views where I want to have the xtype names remain the same even though they might belong in the phone or tablet profiles, I have to remove the alias/xtype config on the class. I then have a profile base class defined like so with a shared helper function:
Ext.define('MyApp.profile.Base', {
extend: 'Ext.app.Profile',
config: {
},
mapViewAliases: function () {
var self = this;
var views = this.getDependencies().view;
var newAliasMap = null;
Ext.each(views, function (view) {
Ext.Array.some(self.getViewsToAliasMap(), function (map) {
if (map[view]) {
if (!newAliasMap) {
newAliasMap = {};
}
newAliasMap[view] = [map[view]];
return true;
}
});
});
if (newAliasMap) {
console.log('view aliases being mapped for: ' + this.$className);
Ext.ClassManager.addNameAliasMappings(newAliasMap)
}
}
});
Then I have the profile class inherit from the base class (this is repeated with the tablet profile except the viewsToAliasMap holds classes belonging to the tablet profile instead of the phone profile):
Ext.define('MyApp.profile.Phone', {
extend: 'MyApp.profile.Base',
config: {
name: 'Phone',
views: ['Login', 'Home', 'Welcome' ],
viewsToAliasMap: [
{ 'MyApp.view.phone.Login': 'widget.loginview' },
{ 'MyApp.view.phone.Home': 'widget.homeview' },
{ 'MyApp.view.phone.Welcome': 'widget.welcomeview' }
]
},
isActive: function () {
return Ext.os.is.Phone;
},
launch: function () {
console.log("Phone profile launched");
this.mapViewAliases();
}
});
So basically, the profile calls the function mapViewAliases() on the base class in the launch function. The mapViewAliases() registers the view class names with the aliases defined in the profile with the class manager. So effectively the xtype names are resolved at run-time.
I'm sure this code can be improved and/or a better way to do this.
Please feel free to let me know.
I am using a pretty naive implementation... I'm sure it could be made more robust, but I've been hacking at this for 5 hours or so now.
Ext.define('MyApp.override.Application', {
override : 'Ext.app.Application',
onProfilesLoaded: function() {
var profiles = this.getProfiles(),
length = profiles.length,
instances = [],
requires = this.gatherDependencies(),
current, i, profileDeps;
for (i = 0; i < length; i++) {
var instance = Ext.create(profiles[i], {
application: this
});
/*
* Note that we actually require all of the dependencies for all Profiles - this is so that we can produce
* a single build file that will work on all defined Profiles. Although the other classes will be loaded,
* the correct Profile will still be identified and the other classes ignored. While this feels somewhat
* inefficient, the majority of the bulk of an application is likely to be the framework itself. The bigger
* the app though, the bigger the effect of this inefficiency so ideally we will create a way to create and
* load Profile-specific builds in a future release.
*
CMK - PSHAW!
*/
if (instance.isActive() && !current) {
console.log('Profile active: ' + instance.getName());
current = instance;
profileDeps = instance.getDependencies();
requires = requires.concat(profileDeps.all);
var ns = instance.getNamespace();
this.setCurrentProfile(current);
// Merge Controllers, Models, Stores, and Views
this.setControllers(this.getControllers().concat(profileDeps.controller));
this.setModels(this.getModels().concat(profileDeps.model));
this.setStores(this.getStores().concat(profileDeps.store));
this.setViews(this.getViews().concat(profileDeps.view));
// Remove the view ref and requires for default views, when a profile specific one exists
Ext.each(profileDeps.view, function(className) {
if (className.indexOf('view.' + ns + '.') !== -1) {
// Requires
var index = requires.indexOf(className.replace('view.' + ns, 'view'));
if (index !== -1) {
requires.splice(index, 1);
}
// Views
index = this.getViews().indexOf(className.replace('view.' + ns, 'view'));
if (index !== -1) {
this.getViews().splice(index, 1);
}
}
}, this);
instances[0] = instance;
break;
}
}
this.setProfileInstances(instances);
Ext.require(requires, this.loadControllerDependencies, this);
}
});
Put this before your Ext.application, and it replaces the profile loader... This one strips out default views with the same name as one in the active profile namespace.
It requires that you define an xtype for the views that match, then even your refs in controllers will work...
I need to continue testing with this, but it looks promising so far.

sencha list paging plugin

I'm trying to use sencha touch's listpaging plugin. But there is almost no good( or bad ) documentation about how to use it and i'm confused.
When i activate the plugin in my list
this.myList = new Ext.List({
store: this.myStore,
plugins: [{
ptype: 'listpaging',
autoPaging: false
}],
itemTpl: '...'
});
a 'Load More' text and a loading image is added to the end of the list.
But i don't know how to configure my store to enable this plugin to be able to load more data.
App.regStore('MyStore', {
model: 'myModel',
proxy: {
type: 'ajax',
url: 'http://mydomain.com/json?howmany=10&page=1',
reader: {
type: 'json',
root: 'results'
}
}
});
App.stores.myStore = Ext.StoreMgr.get('MyStore');
How can i configure my store so when i tap "Load more", the store brings up page 2 and add them to the list automatically?
I've had a similar issue with the ListPaging plugin in SenchaTouch 2, and managed to sort out the 'load more' message behaviour when the end of the data set is reached.
This builds on what John Gordon has come up with (regarding specifying the pageSize and clearOnPageLoad config options), since these properties seem to be the same in Senchatouch 2. I haven't looked at SenchaTouch 1.x in detail. In SenchaTouch 2, all config options must be defined in a config property:
Ext.define('MessagingApp.store.MessageThreads', {
extend : 'Ext.data.Store',
requires: ['MessagingApp.model.MessageThread'],
config:
{
model: 'MessagingApp.model.MessageThread',
autoLoad: false,
clearOnPageLoad: false, // This is true by default
pageSize: 10, // This needs to be set for paging
proxy: {
type: 'jsonp',
pageParam: 'currentPage',
limitParam: 'pageSize',
url: APIURL + '/message-app-service/GetMessageThreads',
reader: {
type: 'json',
rootProperty: 'Data'
}
}
}
});
In the view, where we specify the plugins, we can override the 'load more' and 'no more records' messages:
{
xtype:'dataview',
store:'MessageThreads',
id:'threadList',
itemTpl:Ext.create('Ext.XTemplate',
'<!-- template markup goes here -->',
{
//custom helper functions here
}
),
plugins:[
{
xclass:'Ext.plugin.ListPaging',
autoPaging: true,
// These override the text; use CSS for styling
loadMoreText: 'Loading...',
noMoreRecordsText: 'All messages loaded'
}
]
}
The issue is that while our web service returns an array of records for a particular page, there is no overall total count property, which is needed for the plugin to determine when all records have been loaded. Hence as it is, the 'Load more' message will remain (issue #1 in the accepted solution). So in the init function of our controller, we have to attach an event handler for the load event on the store to hook into when a new page of data is received:
Ext.define('MessagingApp.controller.Messages',
{
extend: 'Ext.app.Controller',
config:
{
views: [
'MessageThreads',
// Other views referenced by this controller
],
stores: [
'MessageThreads'
],
refs:
{
// References to UI elements by selector...
}
},
init: function() {
// Other internal initialisation...
this.control(
{
// Selector-object pairs...
});
// Provide a means to intercept loads of each page of records
var threadStore = Ext.getStore('MessageThreads');
threadStore.addBeforeListener('load', this.checkForThreadListEnd, this);
},
// Remaining controller functions...
});
In the handler, we realise that we've reached the end of the data set when the number of records returned is less than the page size. If the total record count is a multiple of the page size, the last 'page' will have an empty array. Once the end of the data set has been identified, we update the totalCount config property of the store:
checkForThreadListEnd: function(store, records, isSuccessful) {
var pageSize = store.getPageSize();
var pageIndex = store.currentPage - 1; // Page numbers start at 1
if(isSuccessful && records.length < pageSize)
{
//Set count to disable 'loading' message
var totalRecords = pageIndex * pageSize + records.length;
store.setTotalCount(totalRecords);
}
else
store.setTotalCount(null);
},
// Other controller functions...
Because this is a 'before' event handler, this property will be crucially updated before the plugin decides whether to display the 'load more' or 'no more records' messages. Unfortunately, the framework doesn't provide a means to hide the 'no more records' message, so this would have to be done via CSS.
Hope this helps.
I'm having problems finding good documentation, too, but I can at least answer your question. You need to add pageSize to your store, clearOnPageLoad as well, if you want to not clear out what was already loaded. Her's my code:
Ext.regStore('publicresources', {
model: 'PublicResource',
autoLoad:false,
remoteFilter:true,
sortOnFilter:true,
pageSize: 15,
clearOnPageLoad: false,
sorters: [
{
property : 'distance',
direction: 'ASC'
}
]
});
My outstanding issues that I'm looking into are:
How to turn off the "More" element when there are no more records to load
How to detect that there are no more records to load, and where to put that detection code.
How to keep the list at the location that the user was at. Each load jumps back to the 1st item in the list
Good luck!
Remember also that this works only server side currently.
Forum thread http://www.sencha.com/forum/showthread.php?105193-Store-pageSize
In regards to the "load more vs. no more records" message -
If you are writing a custom proxy (example here A Sencha Touch MVC application with PhoneGap), you set the total records in the returned Operation.
If the total records are not yet known, you can do something like the below, where,
1) if you are returning the requested limit of records, set the total to something larger than the records the store will now hold
2) if returning < the requested limit of records, set the total to 1 (to force the "no more records message")
//return model instances in a result set
operation.resultSet = new Ext.data.ResultSet({
records: contacts,
//total : contacts.length,
total : contacts.length === operation._limit ? (operation._limit * operation._page +1) : 1,
loaded : true
});
//announce success
operation.setSuccessful();
operation.setCompleted();
//finish with callback
if (typeof callback == "function") {
callback.call(scope || thisProxy, operation);
}
Just to add what worked for me..
If your server returns a totalCount and you want to set it you can use the totalProperty in the reader
reader: {
type: 'json',
rootProperty: 'results',
totalProperty: 'resultCount'
}

ExtJS 4: Changing Store param names

Right now I'm running into a problem where I can't seem to change the param names page, start, limit, and dir for a Ext.data.Store.
In ExtJS 3 I could do this:
paramNames :
{
start : 'startIndex',
limit : 'pageSize',
sort : 'sortCol',
dir : 'sortDir'
}
I tried adding this configuration to the Ext.data.Store for ExtJS 4 however 'start', 'limit', 'sort', and 'dir' still show up as the default param names. I need to be able to change this as the server side functionality requires these param names. This also causes paging and remote sorting to not work since the param names don't match what the server side resource is expecting.
So is there a new way in ExtJS 4 to change these param names like in ExtJS 3?
take a look at Proxy,
see http://docs.sencha.com/ext-js/4-0/#/api/Ext.data.proxy.Server
directionParam,limitParam...
To dynamically modify the parameters just before the load of a store you can do this:
/* set an additional parameter before loading, not nice but effective */
var p = store.getProxy();
p.extraParams.searchSomething = search;
p.extraParams.somethingelse = 'This works too';
store.load({
scope : this,
callback: function() {
// do something useful here with the results
}
});
Use this code:
proxy: {
type: 'ajax',
url: '/myurl',
method: 'GET',
**extraParams: { myKeyword: 'abcd' },**
reader: {
type: 'json',
root: 'rows'
}
}
Now you can change your myKeyword value from abcd to xyz in following way.
gridDataStore.proxy.extraParams.keyword='xyz';
gridDataStore.load();
this will set your parameters' value and reload the store.
The keys were renamed and moved to the Ext.data.Proxy object. Here's a simple example that tells ExtJS to use the default Grails parameter names:
Ext.create('Ext.data.Store', {
// Other store properties removed for brevity
proxy: {
// Other proxy properties removed for brevity
startParam: "offset",
limitParam: "max",
sortParam: "sort",
directionParam: "order",
simpleSortMode: true
}
});
I also set the simpleSortMode so that each of the parameters are sent to the server as discrete request parameters.