Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d9942ae
Send API request on reference auto-complete
jpetitcolas Jun 15, 2015
d91da77
Add ReferenceFieldView test
jpetitcolas Jun 16, 2015
d7bf2a5
Implement last tests on reference field
jpetitcolas Jun 16, 2015
092d62f
Add remote API call to reference many field
jpetitcolas Jun 16, 2015
118c1a0
Add more tests
jpetitcolas Jun 17, 2015
1d30f8d
Update documentation
jpetitcolas Jun 17, 2015
68ffb96
Fix various issues on reference fields
jpetitcolas Jun 18, 2015
8295a3b
Allow to embed all references in select list, without API requests
jpetitcolas Jun 18, 2015
4f01bb1
Apply map functions on reference labels
jpetitcolas Jun 19, 2015
f4e695d
Fix dependencies
jpetitcolas Jun 19, 2015
f0aa340
Base Reference[Many] fields on Choice[s] fields
jpetitcolas Jun 22, 2015
1d3feec
Create ReferenceRefresher service
jpetitcolas Jun 22, 2015
797be96
Fix last unit tests
jpetitcolas Jun 22, 2015
3e2dcad
Fix e2e tests
jpetitcolas Jun 23, 2015
1f16774
Update configuration
jpetitcolas Jun 23, 2015
6133a65
Start optimizing requests for autocomplete
jpetitcolas Jun 24, 2015
be7832e
Add more unit tests
jpetitcolas Jun 25, 2015
a1578eb
Update reference many field with Refresher getInitialChoices method
jpetitcolas Jun 25, 2015
c895200
Use autocompleteOptions instead of refreshDelay option
jpetitcolas Jun 25, 2015
b312057
Update ui-select version
jpetitcolas Jun 25, 2015
9c95ec5
Rename autocomplete to remoteComplete
jpetitcolas Jun 26, 2015
ae14c31
Fix last tests
jpetitcolas Jun 26, 2015
ac00db7
Update admin-config
jpetitcolas Jun 26, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,8 +710,28 @@ Set the default field for list sorting. Defaults to 'id'
* `sortDir(String)`
Set the default direction for list sorting. Defaults to 'DESC'

* `filters({ field1: value, field2: value, ...])`
Add filters to the referenced results list.
* `filters({ field1: value, field2: value, ...})`
Add filters to the referenced results list. It may be either an object or a function with a single parameter: the current search string.

myView.fields([
nga.field('post_id', 'reference')
.targetEntity(post) // Select a target Entity
.targetField(nga.field('title')) // Select a label Field
.filters(function(search) {
// will send `GET /posts?title=foo%` query
return {
title: search + '%'
};
});
]);

* `remoteComplete([true|false], options = {})`
Enable remote completion. When enabled, it fetches remote API references corresponding to your input to refresh the choices list.
If set to false, all references (in the limit of `perPage` parameter) would be retrieved at view initialization.

Available options are:

* **refreshDelay:** minimal delay between two API calls in milliseconds. By default: 500.

* `perPage(integer)`
Define the maximum number of elements fetched and displayed in the list.
Expand Down Expand Up @@ -746,8 +766,8 @@ Set the default field for list sorting. Defaults to 'id'
* `sortDir(String)`
Set the default direction for list sorting. Defaults to 'DESC'

* `filters({ field1: value, field2: value, ...])`
Add filters to the referenced results list.
* `filters({ field1: value, field2: value, ...})`
Add filters to the referenced results list. It should be an object.

* `perPage(integer)`
Define the maximum number of elements fetched and displayed in the list.
Expand Down Expand Up @@ -781,6 +801,30 @@ Define a function that returns parameters for filtering API calls. You can use i
})
]);

* `filters({ field1: value, field2: value, ...})`
Add filters to the referenced results list. It may be either an object or a function with a single parameter: the current search string.

myView.fields([
nga.field('tags', 'reference_many')
.targetEntity(tag) // Select a target Entity
.targetField(nga.field('name')) // Select a label Field
.filters(function(search) {
// will send `GET /tags?name=foo%&published=true` query
return {
name: search + '%',
published: true
};
});
]);

* `remoteComplete([true|false], options = {})`
Enable remote completion. When enabled, it fetches remote API references corresponding to your input to refresh the choices list.
If set to false, all references (in the limit of `perPage` parameter) would be retrieved at view initialization.

Available options are:

* **refreshDelay:** minimal delay between two API calls in milliseconds. By default: 500.

## Customizing the API Mapping

All HTTP requests made by ng-admin to your REST API are carried out by [Restangular](https://github.com/mgonto/restangular), which is like `$resource` on steroids.
Expand Down
19 changes: 18 additions & 1 deletion examples/blog/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@
nga.field('tags', 'reference_many') // ReferenceMany translates to a select multiple
.targetEntity(tag)
.targetField(nga.field('name'))
.filters(function(search) {
return {
q: search
}
})
.remoteComplete(true, { refreshDelay: 300 })
.cssClasses('col-sm-4'), // customize look and feel through CSS classes
nga.field('pictures', 'json'),
nga.field('views', 'number')
Expand Down Expand Up @@ -179,6 +185,7 @@
.label('Post')
.targetEntity(post)
.targetField(nga.field('title'))
.remoteComplete(true, { refreshDelay: 300 })
])
.listActions(['edit', 'delete']);

Expand All @@ -192,9 +199,19 @@
nga.field('post_id', 'reference')
.label('Post')
.map(truncate)
.filters(function(search) {
if (!search) {
return;
}

return {
q: search // Full-text search
};
})
.targetEntity(post)
.targetField(nga.field('title'))
.validation({ required: true }),
.validation({ required: true })
.remoteComplete(true, { refreshDelay: 0 })
]);

comment.editionView()
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"npm": "^2.10.0"
},
"devDependencies": {
"admin-config": "^0.2.1",
"admin-config": "^0.2.4",
"angular": "~1.3.15",
"angular-bootstrap": "^0.12.0",
"angular-mocks": "^1.3.15",
Expand Down Expand Up @@ -83,11 +83,11 @@
"rangy": "^1.3.0",
"restangular": "^1.5.1",
"sass-loader": "^0.5.0",
"sinon": "^1.14.1",
"sinon": "git://github.com/cjohansen/Sinon.JS#sinon-2.0",
"style-loader": "^0.12.2",
"superagent": "^0.18.2",
"textangular": "^1.3.11",
"ui-select": "^0.11.2",
"ui-select": "angular-ui/ui-select#v0.12.0",
"underscore": "^1.8.3",
"url-loader": "^0.5.5",
"webpack": "^1.9.4",
Expand Down
3 changes: 3 additions & 0 deletions src/javascripts/ng-admin/Crud/CrudModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CrudModule.controller('BatchDeleteController', require('./delete/BatchDeleteCont
CrudModule.service('EntryFormatter', require('./misc/EntryFormatter'));
CrudModule.service('PromisesResolver', require('./misc/PromisesResolver'));
CrudModule.service('ReadQueries', require('./repository/ReadQueries'));
CrudModule.service('ReferenceRefresher', require('./repository/ReferenceRefresher'));
CrudModule.service('WriteQueries', require('./repository/WriteQueries'));

CrudModule.service('RestWrapper', require('./misc/RestWrapper'));
Expand All @@ -28,6 +29,8 @@ CrudModule.directive('maInputField', require('./field/maInputField'));
CrudModule.directive('maJsonField', require('./field/maJsonField'));
CrudModule.directive('maFileField', require('./field/maFileField'));
CrudModule.directive('maCheckboxField', require('./field/maCheckboxField'));
CrudModule.directive('maReferenceField', require('./field/maReferenceField'));
CrudModule.directive('maReferenceManyField', require('./field/maReferenceManyField'));
CrudModule.directive('maTextField', require('./field/maTextField'));
CrudModule.directive('maWysiwygField', require('./field/maWysiwygField'));
CrudModule.directive('maTemplateField', require('./field/maTemplateField'));
Expand Down
31 changes: 22 additions & 9 deletions src/javascripts/ng-admin/Crud/field/maChoiceField.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
function updateChoices(scope, choices) {
scope.choices = choices;
scope.$root.$$phase || scope.$digest();
}

function maChoiceField($compile) {
return {
scope: {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?'
'datastore': '&?',
'refresh': '&'
},
restrict: 'E',
compile: function() {
Expand All @@ -14,21 +20,23 @@ function maChoiceField($compile) {
scope.name = field.name();
scope.v = field.validation();

var refreshAttributes = '';
if (field.type().indexOf('reference') === 0 && field.remoteComplete()) {
scope.refreshDelay = field.remoteCompleteOptions().refreshDelay;
refreshAttributes = 'refresh-delay="refreshDelay" refresh="refresh({ $search: $select.search })"';
}

var choices = field.choices ? field.choices() : [];

var template = `
<ui-select ng-model="$parent.value" ng-required="v.required" id="{{ name }}" name="{{ name }}">
<ui-select-match allow-clear="{{ !v.required }}" placeholder="Filter values">{{ $select.selected.label }}</ui-select-match>
<ui-select-choices repeat="item.value as item in getChoices(entry) | filter: {label: $select.search}">
<ui-select-choices ${refreshAttributes} repeat="item.value as item in choices | filter: {label: $select.search} track by $index">
{{ item.label }}
</ui-select-choices>
</ui-select>`;

var choices;
if (field.type() === 'reference' || field.type() === 'reference_many') {
choices = scope.datastore().getChoices(field);
} else {
choices = field.choices();
}
scope.getChoices = typeof(choices) === 'function' ? choices : function() { return choices; };
scope.choices = typeof(choices) === 'function' ? choices(scope.entry) : choices;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this works, I had to return a function because Angular complained about an infinite look. Needs further testing.

element.html(template);

var select = element.children()[0];
Expand All @@ -38,6 +46,11 @@ function maChoiceField($compile) {
}

$compile(element.contents())(scope);
},
post: function(scope) {
scope.$on('choices:update', function(e, data) {
updateChoices(scope, data.choices);
});
}
};
}
Expand Down
29 changes: 19 additions & 10 deletions src/javascripts/ng-admin/Crud/field/maChoicesField.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ function maChoicesField($compile) {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?'
'datastore': '&?',
'refresh': '&'
},
restrict: 'E',
compile: function() {
Expand All @@ -21,21 +22,23 @@ function maChoicesField($compile) {
scope.name = field.name();
scope.v = field.validation();

var refreshAttributes = '';
if (field.type().indexOf('reference') === 0 && field.remoteComplete()) {
scope.refreshDelay = field.remoteCompleteOptions().refreshDelay;
refreshAttributes = 'refresh-delay="refreshDelay" refresh="refresh({ $search: $select.search })"';
}

var choices = field.choices ? field.choices() : [];

var template = `
<ui-select ${scope.v.required ? 'ui-select-required' : ''} multiple ng-model="$parent.value" ng-required="v.required" id="{{ name }}" name="{{ name }}">
<ui-select-match placeholder="Filter values">{{ $item.label }}</ui-select-match>
<ui-select-choices repeat="item.value as item in getChoices(entry) | filter: {label: $select.search}">
{{ item.label }}
<ui-select-choices ${refreshAttributes} repeat="item.value as item in choices | filter: {label: $select.search}">
{{ item.label }}
</ui-select-choices>
</ui-select>`;

var choices;
if (field.type() === 'reference' || field.type() === 'reference_many') {
choices = scope.datastore().getChoices(field);
} else {
choices = field.choices();
}
scope.getChoices = typeof(choices) === 'function' ? choices : function() { return choices; };
scope.choices = typeof(choices) === 'function' ? choices(scope.entry) : choices;
element.html(template);

var select = element.children()[0];
Expand All @@ -45,6 +48,12 @@ function maChoicesField($compile) {
}

$compile(element.contents())(scope);
},
post: function(scope) {
scope.$on('choices:update', function(e, data) {
scope.choices = data.choices;
scope.$root.$$phase || scope.$digest();
});
}
};
}
Expand Down
45 changes: 45 additions & 0 deletions src/javascripts/ng-admin/Crud/field/maReferenceField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
function maReferenceField(ReferenceRefresher) {
return {
scope: {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?'
},
restrict: 'E',
link: function(scope) {
var field = scope.field();
scope.name = field.name();
scope.v = field.validation();

function refresh(search) {
return ReferenceRefresher.refresh(field, scope.value, search)
.then(formattedResults => {
scope.$broadcast('choices:update', { choices: formattedResults });
});
}

if (field.remoteComplete()) {
ReferenceRefresher.getInitialChoices(field, [scope.value])
.then(options => {
scope.$broadcast('choices:update', { choices: options });
});

scope.refresh = refresh;
} else {
refresh();
}
},
template: `<ma-choice-field
field="field()"
datastore="datastore()"
refresh="refresh($search)"
value="value">
</ma-choice-field>`
};
}

maReferenceField.$inject = ['ReferenceRefresher'];

module.exports = maReferenceField;

56 changes: 56 additions & 0 deletions src/javascripts/ng-admin/Crud/field/maReferenceManyField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
function maReferenceManyField(ReferenceRefresher) {
'use strict';

return {
scope: {
'field': '&',
'value': '=',
'entry': '=?',
'datastore': '&?'
},
restrict: 'E',
link: function(scope) {
var field = scope.field();
scope.name = field.name();
scope.v = field.validation();
scope.choices = [];

function refresh(search) {
return ReferenceRefresher.refresh(field, scope.value, search)
.then(formattedResults => {
scope.$broadcast('choices:update', { choices: formattedResults });
});
}

// if value is set, we should retrieve references label from server
if (scope.value) {
ReferenceRefresher.getInitialChoices(field, scope.value)
.then(options => {
scope.$broadcast('choices:update', { choices: options });

if (field.remoteComplete()) {
scope.refresh = refresh;
} else {
refresh();
}
});
} else {
if (field.remoteComplete()) {
scope.refresh = refresh;
} else {
refresh();
}
}
},
template: `<ma-choices-field
field="field()"
datastore="datastore()"
refresh="refresh($search)"
value="value">
</ma-choice-field>`
};
}

maReferenceManyField.$inject = ['ReferenceRefresher'];

module.exports = maReferenceManyField;
Loading