Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFR] Fix double call on reference fields #732

Merged
merged 4 commits into from
Oct 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 26 additions & 24 deletions doc/Configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,18 +658,18 @@ If set to false, all references (in the limit of `perPage` parameter) would be r
* `refreshDelay`: minimal delay between two API calls in milliseconds. By default: 500.
* `searchQuery`: a function returning the parameters to add to the query string basd on the input string.

comments.editionView().fields([
nga.field('id'),
nga.field('post_id', 'reference')
.targetEntity(post)
.targetField(nga.field('title'))
.remoteComplete(true, {
refreshDelay: 300,
// populate choices from the response of GET /posts?q=XXX
searchQuery: function(search) { return { q: search }; }
})
.perPage(10) // limit the number of results to 10
]);
comments.editionView().fields([
nga.field('id'),
nga.field('post_id', 'reference')
.targetEntity(post)
.targetField(nga.field('title'))
.remoteComplete(true, {
refreshDelay: 300,
// populate choices from the response of GET /posts?q=XXX
searchQuery: function(search) { return { q: search }; }
})
.perPage(10) // limit the number of results to 10
]);

* `permanentFilters({ field1: value, field2: value, ...})`
Add filters to the referenced results list. This can be very useful to restrict the list of possible values displayed in a dropdown list:
Expand Down Expand Up @@ -752,6 +752,8 @@ Define a function that returns parameters for filtering API calls. You can use i
})
]);

**Tip**: It also works for `creationView` and `editionView`

* `permanentFilters({ field1: value, field2: value, ...})`
Add filters to the referenced results list.

Expand All @@ -772,15 +774,15 @@ If set to false, all references (in the limit of `perPage` parameter) would be r
* `refreshDelay`: minimal delay between two API calls in milliseconds. By default: 500.
* `searchQuery`: a function returning the parameters to add to the query string basd on the input string.

post.editionView().fields([
nga.field('id'),
nga.field('tags', 'reference_many')
.targetEntity(tag)
.targetField(nga.field('name'))
.remoteComplete(true, {
refreshDelay: 300,
// populate choices from the response of GET /tags?q=XXX
searchQuery: function(search) { return { q: search }; }
})
.perPage(10) // limit the number of results to 10
]);
post.editionView().fields([
nga.field('id'),
nga.field('tags', 'reference_many')
.targetEntity(tag)
.targetField(nga.field('name'))
.remoteComplete(true, {
refreshDelay: 300,
// populate choices from the response of GET /tags?q=XXX
searchQuery: function(search) { return { q: search }; }
})
.perPage(10) // limit the number of results to 10
]);
1 change: 1 addition & 0 deletions examples/blog/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
refreshDelay: 300 ,
searchQuery: function(search) { return { q: search }; }
})
.singleApiCall(ids => { return {'id': ids }; })
.cssClasses('col-sm-4'), // customize look and feel through CSS classes
nga.field('pictures', 'json'),
nga.field('views', 'number')
Expand Down
2 changes: 1 addition & 1 deletion src/javascripts/ng-admin/Crud/field/maChoiceField.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function maChoiceField($compile) {
});

var refreshAttributes = '';
if (field.type().indexOf('reference') === 0 && field.remoteComplete()) {
if (field.type().indexOf('reference') === 0 && field.remoteComplete()) { // FIXME wrong place to do that
scope.refreshDelay = field.remoteCompleteOptions().refreshDelay;
refreshAttributes = 'refresh-delay="refreshDelay" refresh="refresh({ $search: $select.search })"';
}
Expand Down
43 changes: 27 additions & 16 deletions src/javascripts/ng-admin/Crud/field/maReferenceField.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,37 @@ function maReferenceField(ReferenceRefresher) {
},
restrict: 'E',
link: function(scope) {
var field = scope.field();
const field = scope.field();
const identifierName = field.targetEntity().identifier().name()
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;
if (!field.remoteComplete()) {
// fetch choices from the datastore
let initialEntries = scope.datastore()
.getEntries(field.targetEntity().uniqueId + '_choices');
const isCurrentValueInInitialEntries = initialEntries.filter(e => e.identifierValue === scope.value).length > 0;
if (scope.value && !isCurrentValueInInitialEntries) {
initialEntries.push(scope.datastore()
.getEntries(field.targetEntity().uniqueId + '_values')
.filter(entry => entry.values[identifierName] == scope.value)
.pop()
);
}
const initialChoices = initialEntries.map(entry => ({
value: entry.values[identifierName],
label: entry.values[field.targetField().name()]
}));
scope.$broadcast('choices:update', { choices: initialChoices });
} else {
refresh();
// ui-select doesn't allow to prepopulate autocomplete selects, see https://github.com/angular-ui/ui-select/issues/1197
// let ui-select fetch the options using the ReferenceRefresher
scope.refresh = function refresh(search) {
return ReferenceRefresher.refresh(field, scope.value, search)
.then(formattedResults => {
scope.$broadcast('choices:update', { choices: formattedResults });
});
};
}
},
template: `<ma-choice-field
Expand Down
57 changes: 35 additions & 22 deletions src/javascripts/ng-admin/Crud/field/maReferenceManyField.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,49 @@ function maReferenceManyField(ReferenceRefresher) {
},
restrict: 'E',
link: function(scope) {
var field = scope.field();
const field = scope.field();
const identifierName = field.targetEntity().identifier().name();
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 });
const setInitialChoices = (initialEntries) => {
if (scope.value && scope.value.length) {
scope.value.map((value) => {
const isCurrentValueInInitialEntries = initialEntries.filter(e => e.identifierValue === value).length > 0;
if (value && !isCurrentValueInInitialEntries) {
initialEntries.push(scope.datastore()
.getEntries(field.targetEntity().uniqueId + '_values')
.filter(entry => entry.values[identifierName] == value)
.pop()
);
}
});
}
const initialChoices = initialEntries.map(entry => ({
value: entry.values[identifierName],
label: entry.values[field.targetField().name()]
}));
scope.$broadcast('choices:update', { choices: initialChoices });
}

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

if (field.remoteComplete()) {
scope.refresh = refresh;
} else {
refresh();
}
});
if (!field.remoteComplete()) {
// fetch choices from the datastore
const initialEntries = scope.datastore()
.getEntries(field.targetEntity().uniqueId + '_choices');
setInitialChoices(initialEntries);
} else {
if (field.remoteComplete()) {
scope.refresh = refresh;
} else {
refresh();
}
const initialEntries = [];
setInitialChoices(initialEntries);

// ui-select doesn't allow to prepopulate autocomplete selects, see https://github.com/angular-ui/ui-select/issues/1197
// let ui-select fetch the options using the ReferenceRefresher
scope.refresh = (search) => {
return ReferenceRefresher.refresh(field, scope.value, search)
.then(formattedResults => {
scope.$broadcast('choices:update', { choices: formattedResults });
});
};
}
},
template: `<ma-choices-field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ class ReferenceRefresher {
return promise;
}

getInitialChoices(field, values) {
return this.ReadQueries.getRecordsByIds(field.targetEntity(), values)
.then(results => this._removeDuplicates(results, values))
.then(records => this._transformRecords(field, records));
}

_removeDuplicates(results, currentValue) {
// remove already assigned values: ui-select still return them if multiple
if (!currentValue) {
Expand Down
6 changes: 0 additions & 6 deletions src/javascripts/test/unit/Crud/field/maReferenceFieldSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ describe('ReferenceField', function() {
beforeEach(function() {
angular.mock.module(function($provide) {
$provide.service('ReferenceRefresher', function($q) {
this.getInitialChoices = jasmine.createSpy('getInitialChoices').and.callFake(function() {
return mixins.buildPromise([
{ value: 2, label: 'bar' }
]);
});

this.refresh = jasmine.createSpy('refresh').and.callFake(function() {
return mixins.buildPromise([
{ value: 1, label: 'foo' },
Expand Down
35 changes: 26 additions & 9 deletions src/javascripts/test/unit/Crud/field/maReferenceManyFieldSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,14 @@ describe('ReferenceManyField', function() {
var ReferenceManyField = require('admin-config/lib/Field/ReferenceManyField');
var mixins = require('../../../mock/mixins');
var DataStore = require('admin-config/lib/DataStore/DataStore');
var Entry = require('admin-config/lib/Entry');

var $compile, $timeout, scope;
const directiveUsage = '<ma-reference-many-field entry="entry" field="field" value="value" datastore="datastore"></ma-reference-many-field>';

beforeEach(function() {
angular.mock.module(function($provide) {
$provide.service('ReferenceRefresher', function($q) {
this.getInitialChoices = jasmine.createSpy('getInitialChoices').and.callFake(function() {
return mixins.buildPromise([
{ value: 2, label: 'bar' },
{ value: 3, label: 'qux' }
]);
});

$provide.service('ReferenceRefresher', function() {
this.refresh = jasmine.createSpy('refresh').and.callFake(function() {
return mixins.buildPromise([
{ value: 1, label: 'foo' },
Expand All @@ -44,12 +38,23 @@ describe('ReferenceManyField', function() {
}));

beforeEach(function() {
scope.datastore = new DataStore();
scope.datastore = {
getEntries: (name) => {
if (name === 'tag_1_choices') {
return [
new Entry('tag', { id: 1, name: 'foo' }, 1),
new Entry('tag', { id: 2, name: 'bar' }, 2),
new Entry('tag', { id: 3, name: 'qux' }, 3),
];
}
}
};
scope.field = new ReferenceManyField('tags')
.targetField({
name: () => 'name'
})
.targetEntity({
uniqueId: 'tag_1',
identifier: () => {
return {
name: () => 'id'
Expand All @@ -67,6 +72,8 @@ describe('ReferenceManyField', function() {
});

it('should call remote API when inputting first characters', function () {
scope.field.remoteComplete(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

This one was missing before our changes.


var element = $compile(directiveUsage)(scope);
scope.$digest();

Expand All @@ -84,6 +91,16 @@ describe('ReferenceManyField', function() {
]));
});

it('should refresh not called if remote complete is null', function() {
scope.field.remoteComplete(false);

var element = $compile(directiveUsage)(scope);
$timeout.flush();
scope.$digest();

expect(MockedReferenceRefresher.refresh).not.toHaveBeenCalled();
});

it('should get all choices loaded at initialization if remote complete is null', function() {
scope.field.remoteComplete(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,52 +135,4 @@ describe('ReferenceRefresher', function() {
});
});
});

describe('getInitialChoices', function() {
var readQueries, refresher;
beforeEach(function() {
readQueries = new ReadQueries();
var spy = spyOn(readQueries, 'getRecordsByIds');
spy.and.returnValue(mixins.buildPromise([
{ id: 1, title: 'Discover some awesome stuff' },
{ id: 2, title: 'Another great post'}
]));

refresher = new ReferenceRefresher(readQueries);
});

it('should retrieve correct labels from given values, in correct choices expected format', function(done) {
refresher.getInitialChoices(fakeField, [1, 2]).then(function(results) {
expect(readQueries.getRecordsByIds).toHaveBeenCalled();
expect(results).toEqual([
{ value: 1, label: 'Discover some awesome stuff' },
{ value: 2, label: 'Another great post' }
]);
done();
});
});

it('should return mapped values for labels', function(done) {
fakeField = {
name: () => 'post',
targetEntity: () => fakeEntity,
targetField: () => {
return {
name: () => 'title',
flattenable: () => false,
getMappedValue: (v, e) => `${e.title} (#${e.id})`
}
},
type: () => 'reference'
};
refresher.getInitialChoices(fakeField, [1, 2]).then(function(results) {
expect(results).toEqual([
{ value: 1, label: 'Discover some awesome stuff (#1)' },
{ value: 2, label: 'Another great post (#2)' }
]);

done();
});
});
});
});