├── .npmignore
├── .gitignore
├── .travis.yml
├── src
├── View
│ ├── ShowView.js
│ ├── ExportView.js
│ ├── BatchDeleteView.js
│ ├── DashboardView.js
│ ├── MenuView.js
│ ├── DeleteView.js
│ ├── EditView.js
│ ├── CreateView.js
│ ├── ListView.js
│ └── View.js
├── Field
│ ├── EmailField.js
│ ├── TextField.js
│ ├── PasswordField.js
│ ├── ChoicesField.js
│ ├── JsonField.js
│ ├── FloatField.js
│ ├── TemplateField.js
│ ├── ReferenceManyField.js
│ ├── DateTimeField.js
│ ├── FileField.js
│ ├── WysiwygField.js
│ ├── ChoiceField.js
│ ├── BooleanField.js
│ ├── NumberField.js
│ ├── DateField.js
│ ├── ReferencedListField.js
│ ├── EmbeddedListField.js
│ ├── ReferenceField.js
│ └── Field.js
├── Queries
│ ├── Queries.js
│ ├── WriteQueries.js
│ └── ReadQueries.js
├── Utils
│ ├── orderElement.js
│ ├── stringUtils.js
│ ├── ReferenceExtractor.js
│ ├── PromisesResolver.js
│ └── objectProperties.js
├── Collection.js
├── Dashboard.js
├── Entry.js
├── DataStore
│ └── DataStore.js
├── Menu
│ └── Menu.js
├── Factory.js
├── Entity
│ └── Entity.js
└── Application.js
├── Makefile
├── tests
├── mock
│ ├── PromisesResolver.js
│ └── mixins.js
└── lib
│ ├── Utils
│ ├── stringUtilsTest.js
│ ├── orderElementTest.js
│ ├── PromisesResolverTest.js
│ ├── ReferenceExtractorTest.js
│ └── objectPropertiesTest.js
│ ├── Field
│ ├── TemplateFieldTest.js
│ ├── ReferencedListFieldTest.js
│ ├── ReferenceFieldTest.js
│ └── FieldTest.js
│ ├── View
│ ├── MenuViewTest.js
│ ├── ListViewTest.js
│ └── ViewTest.js
│ ├── DashboardTest.js
│ ├── FactoryTest.js
│ ├── Queries
│ ├── WriteQueriesTest.js
│ └── ReadQueriesTest.js
│ ├── Entity
│ └── EntityTest.js
│ ├── DataStore
│ └── DataStore.js
│ ├── EntryTest.js
│ └── Menu
│ └── MenuTest.js
├── package.json
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | node_modules/
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.12"
4 | sudo: false
5 | cache:
6 | directories:
7 | - node_modules
8 | branches:
9 | only:
10 | - master
11 |
--------------------------------------------------------------------------------
/src/View/ShowView.js:
--------------------------------------------------------------------------------
1 | import View from './View';
2 |
3 | class ShowView extends View {
4 | constructor(name) {
5 | super(name);
6 | this._type = 'ShowView';
7 | }
8 | }
9 |
10 | export default ShowView;
11 |
--------------------------------------------------------------------------------
/src/Field/EmailField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class EmailField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "email";
7 | }
8 | }
9 |
10 | export default EmailField;
11 |
--------------------------------------------------------------------------------
/src/Field/TextField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class TextField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "text";
7 | }
8 | }
9 |
10 | export default TextField;
11 |
--------------------------------------------------------------------------------
/src/Field/PasswordField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class PasswordField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "password";
7 | }
8 | }
9 |
10 | export default PasswordField;
11 |
--------------------------------------------------------------------------------
/src/Field/ChoicesField.js:
--------------------------------------------------------------------------------
1 | import ChoiceField from "./ChoiceField";
2 |
3 | class ChoicesField extends ChoiceField {
4 | constructor(name) {
5 | super(name);
6 | this._type = "choices";
7 | }
8 | }
9 |
10 | export default ChoicesField;
11 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | @npm install
3 |
4 | transpile:
5 | @mkdir -p lib/
6 | @rm -rf lib/*
7 | @./node_modules/.bin/babel src/ -d lib/ --source-maps > /dev/null
8 |
9 | test:
10 | @./node_modules/mocha/bin/mocha --compilers js:babel/register --recursive tests/
11 |
--------------------------------------------------------------------------------
/src/Field/JsonField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class JsonField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "json";
7 | this._flattenable = false;
8 | }
9 | }
10 |
11 | export default JsonField;
12 |
--------------------------------------------------------------------------------
/tests/mock/PromisesResolver.js:
--------------------------------------------------------------------------------
1 |
2 | import buildPromise from "./mixins";
3 |
4 | var PromisesResolver = {
5 | allEvenFailed: function() { return buildPromise([]); },
6 | empty: function() { return buildPromise(); }
7 | };
8 |
9 | export default PromisesResolver;
10 |
--------------------------------------------------------------------------------
/src/View/ExportView.js:
--------------------------------------------------------------------------------
1 | import ListView from './ListView';
2 |
3 | class ExportView extends ListView {
4 | constructor(name) {
5 | super(name);
6 | this._fields = [];
7 | this._type = 'ExportView';
8 | }
9 | }
10 |
11 | export default ExportView;
12 |
--------------------------------------------------------------------------------
/src/Field/FloatField.js:
--------------------------------------------------------------------------------
1 | import NumberField from "./NumberField";
2 |
3 | class FloatField extends NumberField {
4 | constructor(name) {
5 | super(name);
6 | this._type = 'float';
7 | this._format = '0.000';
8 | }
9 | }
10 |
11 | export default FloatField;
12 |
--------------------------------------------------------------------------------
/src/Field/TemplateField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class TemplateField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "template";
7 | this._flattenable = false;
8 | }
9 | }
10 |
11 | export default TemplateField;
12 |
--------------------------------------------------------------------------------
/src/Field/ReferenceManyField.js:
--------------------------------------------------------------------------------
1 | import ReferenceField from "./ReferenceField";
2 |
3 | class ReferenceManyField extends ReferenceField {
4 | constructor(name) {
5 | super(name);
6 | this._type = 'reference_many';
7 | }
8 | }
9 |
10 | export default ReferenceManyField;
11 |
--------------------------------------------------------------------------------
/src/View/BatchDeleteView.js:
--------------------------------------------------------------------------------
1 | import View from './View';
2 |
3 | class BatchDeleteView extends View {
4 | constructor(name) {
5 | super(name);
6 |
7 | this._type = 'BatchDeleteView';
8 | this._enabled = true;
9 | }
10 | }
11 |
12 | export default BatchDeleteView;
13 |
--------------------------------------------------------------------------------
/src/Queries/Queries.js:
--------------------------------------------------------------------------------
1 |
2 | class Queries {
3 | constructor(RestWrapper, PromisesResolver, Application) {
4 | this._restWrapper = RestWrapper;
5 | this._promisesResolver = PromisesResolver;
6 | this._application = Application;
7 | }
8 | }
9 |
10 | export default Queries;
11 |
--------------------------------------------------------------------------------
/src/Utils/orderElement.js:
--------------------------------------------------------------------------------
1 | export default {
2 | order: function (input) {
3 | var results = [],
4 | objectKey;
5 |
6 | for (objectKey in input) {
7 | results.push(input[objectKey]);
8 | }
9 |
10 | return results.sort((e1, e2) => e1.order() - e2.order());
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/Collection.js:
--------------------------------------------------------------------------------
1 | import ListView from './View/ListView';
2 |
3 | class Collection extends ListView {
4 |
5 | setEntity(entity) {
6 | this.entity = entity;
7 | if (!this._name) {
8 | this._name = entity.name();
9 | }
10 | return this;
11 | }
12 | }
13 |
14 | export default Collection;
15 |
--------------------------------------------------------------------------------
/src/View/DashboardView.js:
--------------------------------------------------------------------------------
1 | import ListView from './ListView';
2 |
3 | class DashboardView extends ListView {
4 | setEntity(entity) {
5 | this.entity = entity;
6 | if (!this._name) {
7 | this._name = entity.name();
8 | }
9 | return this;
10 | }
11 | }
12 |
13 | export default DashboardView;
14 |
--------------------------------------------------------------------------------
/src/Field/DateTimeField.js:
--------------------------------------------------------------------------------
1 | import DateField from "./DateField";
2 |
3 | class DateTimeField extends DateField {
4 | constructor(name) {
5 | super(name);
6 |
7 | this._format = null;
8 | this._parse = function(date) {
9 | return date;
10 | };
11 |
12 | this._type = 'datetime';
13 | }
14 | }
15 |
16 | export default DateTimeField;
17 |
--------------------------------------------------------------------------------
/src/Field/FileField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class FileField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "file";
7 | this._uploadInformation = {
8 | url: '/upload',
9 | accept: '*'
10 | };
11 | }
12 |
13 | uploadInformation(information) {
14 | if (!arguments.length) return this._uploadInformation;
15 | this._uploadInformation = information;
16 | return this;
17 | }
18 | }
19 |
20 | export default FileField;
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "admin-config",
3 | "version": "0.12.4",
4 | "private": false,
5 | "files": [
6 | "*.md",
7 | "lib"
8 | ],
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/marmelab/admin-config.git"
12 | },
13 | "engines": {
14 | "node": ">=0.10.0"
15 | },
16 | "devDependencies": {
17 | "babel": "^5.5.3",
18 | "chai": "^2.3.0",
19 | "mocha": "^2.2.5",
20 | "sinon": "^1.14.1"
21 | },
22 | "scripts": {
23 | "prepublish": "make transpile",
24 | "test": "make test"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Utils/stringUtils.js:
--------------------------------------------------------------------------------
1 | export default {
2 | /**
3 | * @see http://stackoverflow.com/questions/10425287/convert-string-to-camelcase-with-regular-expression
4 | * @see http://phpjs.org/functions/ucfirst/
5 | */
6 | camelCase: function(text) {
7 | if (!text) {
8 | return text;
9 | }
10 |
11 | let f = text.charAt(0).toUpperCase();
12 | text = f + text.substr(1);
13 |
14 | return text.replace(/[-_.\s](.)/g, function (match, group1) {
15 | return ' ' + group1.toUpperCase();
16 | });
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/Field/WysiwygField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class WysiwygField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "wysiwyg";
7 | this._stripTags = false;
8 | this._sanitize = true;
9 | }
10 |
11 | stripTags(value) {
12 | if (!arguments.length) return this._stripTags;
13 | this._stripTags = value;
14 | return this;
15 | }
16 |
17 | sanitize(value) {
18 | if (!arguments.length) return this._sanitize;
19 | this._sanitize = value;
20 | return this;
21 | }
22 | }
23 |
24 | export default WysiwygField;
25 |
--------------------------------------------------------------------------------
/tests/mock/mixins.js:
--------------------------------------------------------------------------------
1 | function buildPromise(output) {
2 | return (function() {
3 | var result;
4 | return {
5 | 'then': (cb) => {
6 | result = cb(output);
7 |
8 | if (result && result.then) {
9 | // The result is already a promise, just return it
10 | return result;
11 | }
12 |
13 | // We chain the result into a new promise
14 | return buildPromise(result);
15 | },
16 | 'finally': (cb) => {
17 | cb();
18 | return this;
19 | }
20 | };
21 | }());
22 | }
23 |
24 | export default buildPromise;
25 |
--------------------------------------------------------------------------------
/src/Field/ChoiceField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class ChoiceField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "choice";
7 | this._choices = [];
8 | }
9 |
10 | choices(choices) {
11 | if (!arguments.length) return this._choices;
12 | this._choices = choices;
13 |
14 | return this;
15 | }
16 |
17 | getLabelForChoice(value, entry) {
18 | let choices = typeof(this._choices) === 'function' ? this._choices(entry) : this._choices;
19 | let choice = choices.filter(c => c.value == value).pop();
20 | return choice ? choice.label : null;
21 | }
22 | }
23 |
24 | export default ChoiceField;
25 |
--------------------------------------------------------------------------------
/tests/lib/Utils/stringUtilsTest.js:
--------------------------------------------------------------------------------
1 | const assert = require('chai').assert;
2 |
3 | import {camelCase} from '../../../lib/Utils/stringUtils';
4 |
5 | describe('StringUtils', () => {
6 | describe('camelCase()', () => {
7 | it('should not change an already camel-cased string', () => assert.equal(camelCase('Foo'), 'Foo'));
8 | it('should not change an uppercase string', () => assert.equal(camelCase('FOO'), 'FOO'));
9 | it('should camel-case a lowercase string', () => assert.equal(camelCase('foo'), 'Foo'));
10 | it('should camel-case all words string', () => assert.equal(camelCase('foo bar'), 'Foo Bar'));
11 | it('should camel-case all sub strings', () => assert.equal(camelCase('foo_bar-baz.foo'), 'Foo Bar Baz Foo'));
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/Field/BooleanField.js:
--------------------------------------------------------------------------------
1 | import ChoiceField from "./ChoiceField";
2 |
3 | class BooleanField extends ChoiceField {
4 | constructor(name) {
5 | super(name);
6 | this._type = "boolean";
7 | this._choices = [
8 | { value: null, label: 'undefined' },
9 | { value: true, label: 'true' },
10 | { value: false, label: 'false' }
11 | ];
12 | this._filterChoices = [
13 | { value: true, label: 'true' },
14 | { value: false, label: 'false' }
15 | ];
16 | }
17 |
18 | filterChoices(filterChoices) {
19 | if (!arguments.length) return this._filterChoices;
20 | this._filterChoices = filterChoices;
21 |
22 | return this;
23 | }
24 | }
25 |
26 | export default BooleanField;
27 |
--------------------------------------------------------------------------------
/src/View/MenuView.js:
--------------------------------------------------------------------------------
1 | import View from "./View";
2 |
3 | class MenuView extends View {
4 | constructor(name) {
5 | super(name);
6 | this._type = 'MenuView';
7 | this._icon = null;
8 | }
9 |
10 | get enabled() {
11 | return this._enabled === null ? this.entity._views['ListView'].enabled : this._enabled;
12 | }
13 |
14 | icon() {
15 | if (arguments.length) {
16 | console.warn('entity.menuView() is deprecated. Please use the Menu class instead');
17 | this._icon = arguments[0];
18 | return this;
19 | }
20 |
21 | if (this._icon === null) {
22 | return '';
23 | }
24 |
25 | return this._icon;
26 | }
27 | }
28 |
29 | export default MenuView;
30 |
--------------------------------------------------------------------------------
/src/Dashboard.js:
--------------------------------------------------------------------------------
1 | class Dashboard {
2 | constructor() {
3 | this._collections = {};
4 | this._template = null;
5 | }
6 |
7 | addCollection(collection) {
8 | this._collections[collection.name()] = collection;
9 | return this;
10 | }
11 |
12 | collections(collections) {
13 | if (arguments.length) {
14 | this._collections = collections;
15 | return this;
16 | }
17 | return this._collections;
18 | }
19 |
20 | hasCollections() {
21 | return Object.keys(this._collections).length > 0;
22 | }
23 |
24 | template(template) {
25 | if (arguments.length) {
26 | this._template = template;
27 | return this;
28 | }
29 | return this._template;
30 | }
31 | }
32 |
33 | export default Dashboard;
34 |
--------------------------------------------------------------------------------
/tests/lib/Utils/orderElementTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import orderElement from "../../../lib/Utils/orderElement";
4 |
5 | describe('orderElement', () => {
6 |
7 | describe("order()", () => {
8 |
9 | it('should order all elements', () => {
10 | var elements = [
11 | {order: function () { return 1; }, name: 'field1'},
12 | {order: function () { return 0; }, name: 'field2'},
13 | {order: function () { return 3; }, name: 'field3'}
14 | ];
15 |
16 | var orderedElements = orderElement.order(elements);
17 |
18 | // Check that elements are ordered
19 | assert.equal(orderedElements.length, 3);
20 | assert.equal(orderedElements[0].name, 'field2');
21 | assert.equal(orderedElements[1].name, 'field1');
22 | assert.equal(orderedElements[2].name, 'field3');
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/Field/NumberField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class NumberField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = "number";
7 | this._format = undefined;
8 | }
9 |
10 | /**
11 | * Specify format pattern for number to string conversion.
12 | *
13 | * Based on NumeralJs, which uses a syntax similar to Excel.
14 | *
15 | * {@link} http://numeraljs.com/
16 | * {@link} https://github.com/baumandm/angular-numeraljs
17 | * {@example}
18 | *
19 | * nga.field('height', 'number').format('$0,0.00');
20 | */
21 | format(value) {
22 | if (!arguments.length) return this._format;
23 | this._format = value;
24 | return this;
25 | }
26 |
27 | fractionSize(decimals) {
28 | console.warn('NumberField.fractionSize() is deprecated, use NumberField.format() instead');
29 | this.format('0.' + '0'.repeat(decimals));
30 | return this;
31 | }
32 |
33 | }
34 |
35 | export default NumberField;
36 |
--------------------------------------------------------------------------------
/tests/lib/Field/TemplateFieldTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import TemplateField from "../../../lib/Field/TemplateField";
4 |
5 | describe('TemplateField', function() {
6 | describe('template()', function() {
7 | it('should accept string values', function () {
8 | var field = new TemplateField().template('hello!');
9 | assert.equal(field.getTemplateValue(), 'hello!');
10 | });
11 |
12 | it('should accept function values', function () {
13 | var field = new TemplateField().template(function () { return 'hello function !'; });
14 | assert.equal(field.getTemplateValue(), 'hello function !');
15 | });
16 | });
17 |
18 | describe('getTemplateValue()', function() {
19 | it('should return the template function executed with the supplied data', function() {
20 | var field = new TemplateField().template(function (name) { return 'hello ' + name + ' !'; });
21 | assert.equal(field.getTemplateValue('John'), 'hello John !');
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/Field/DateField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class DateField extends Field {
4 | constructor(name) {
5 | super(name);
6 |
7 | this._format = null;
8 | this._parse = function(date) {
9 | if (date instanceof Date) {
10 | // the datepicker returns a JS Date object, with hours, minutes and timezone
11 | // in order to convert it back to date, we must remove the timezone, then
12 | // remove hours and minutes
13 | date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
14 |
15 | let dateString = date.toJSON();
16 | return dateString ? dateString.substr(0,10) : null;
17 | }
18 | return date;
19 | };
20 | this._type = "date";
21 | }
22 |
23 | format(value) {
24 | if (!arguments.length) return this._format;
25 | this._format = value;
26 | return this;
27 | }
28 |
29 | parse(value) {
30 | if (!arguments.length) return this._parse;
31 | this._parse = value;
32 | return this;
33 | }
34 | }
35 |
36 | export default DateField;
37 |
--------------------------------------------------------------------------------
/src/Utils/ReferenceExtractor.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | getReferencedLists(fields) {
4 | return this.indexByName(fields.filter(f => f.type() === 'referenced_list'));
5 | },
6 | getReferences(fields, withRemoteComplete, optimized = null) {
7 | let references = fields.filter(f => f.type() === 'reference' || f.type() === 'reference_many');
8 | if (withRemoteComplete === true) {
9 | references = references.filter(r => r.remoteComplete());
10 | } else if (withRemoteComplete === false) {
11 | references = references.filter(r => !r.remoteComplete());
12 | }
13 | if (optimized !== null) {
14 | references = references.filter(r => r.hasSingleApiCall() === optimized)
15 | }
16 | return this.indexByName(references);
17 | },
18 | getNonOptimizedReferences(fields, withRemoteComplete) {
19 | return this.getReferences(fields, withRemoteComplete, false);
20 | },
21 | getOptimizedReferences(fields, withRemoteComplete) {
22 | return this.getReferences(fields, withRemoteComplete, true);
23 | },
24 | indexByName(references) {
25 | return references.reduce((referencesByName, reference) => {
26 | referencesByName[reference.name()] = reference;
27 | return referencesByName;
28 | }, {});
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # admin-config [](https://travis-ci.org/marmelab/admin-config)
2 |
3 | Common files used in both [ng-admin](https://github.com/marmelab/ng-admin) and [ng-admin-react](https://github.com/marmelab/ng-admin-react).
4 |
5 | ## Installation
6 |
7 | ```sh
8 | make install
9 | ```
10 |
11 | ## Including In Another Library
12 |
13 | Require whatever class you need directly.
14 |
15 | ```js
16 | // es5
17 | var NumberField = require('admin-config/lib/Field/NumberField');
18 | // es6
19 | import NumberField from "admin-config/lib/Field/NumberField";
20 | ```
21 |
22 | Admin-config is written in ES6. You'll need a transpiler to use any of the classes (we recommend [Webpack](http://webpack.github.io/) and [babel](https://babeljs.io/)). Here is an example Webpack configuration:
23 |
24 | ```js
25 | module.exports = {
26 | // ...
27 | module: {
28 | loaders: [
29 | { test: /node_modules\/admin-config\/.*\.js$/, loader: 'babel' }
30 | ]
31 | }
32 | };
33 | ```
34 |
35 | ## Transpiling
36 |
37 | In order to increase this library compatibility and to not force other users of this
38 | library to use Babel, you need to transpile your ES6 code from `src/` to good old ES5
39 | code (in `lib/`).
40 |
41 | Just run:
42 |
43 | ``` sh
44 | make transpile
45 | ```
46 |
47 | And you are done!
48 |
49 | ## Running Tests
50 |
51 | ```sh
52 | make test
53 | ```
54 |
--------------------------------------------------------------------------------
/src/Utils/PromisesResolver.js:
--------------------------------------------------------------------------------
1 |
2 | class PromisesResolver {
3 | static empty(value) {
4 | return new Promise((resolve) => {
5 | resolve(value);
6 | });
7 | }
8 |
9 | static allEvenFailed(promises) {
10 | if (!Array.isArray(promises)) {
11 | throw Error('allEvenFailed can only handle an array of promises');
12 | }
13 |
14 | return new Promise((resolve, reject) => {
15 | if (promises.length === 0) {
16 | return resolve([]);
17 | }
18 |
19 | let states = [],
20 | results = [];
21 |
22 | promises.forEach((promise, key) => {
23 | states[key] = false; // promises are not resolved by default
24 | });
25 |
26 | promises.forEach((promise, key) => {
27 | function resolveState(result) {
28 | states[key] = true;
29 | results[key] = result; // result may be an error
30 | for (let i in states) {
31 | if (!states[i]) {
32 | return;
33 | }
34 | }
35 |
36 | resolve(results);
37 | }
38 |
39 | function resolveSuccess(result) {
40 | return resolveState({status: 'success', result: result});
41 | }
42 |
43 | function resolveError(result) {
44 | return resolveState({status: 'error', error: result})
45 | }
46 |
47 | // whether the promise ends with success or error, consider it done
48 | promise.then(resolveSuccess, resolveError);
49 | });
50 | });
51 | }
52 | }
53 |
54 | export default PromisesResolver;
55 |
--------------------------------------------------------------------------------
/tests/lib/Utils/PromisesResolverTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 | var expect = require('chai').expect;
3 |
4 | import PromisesResolver from "../../../lib/Utils/PromisesResolver";
5 |
6 | describe('PromisesResolver', () => {
7 |
8 | describe("allEvenFailed()", () => {
9 |
10 | it('should throw an exception when the argument is not an array', () => {
11 | var error = 'allEvenFailed can only handle an array of promises';
12 |
13 | expect(() => { PromisesResolver.allEvenFailed('nope') }).to.throw(error);
14 | expect(() => { PromisesResolver.allEvenFailed() }).to.throw(error);
15 | expect(() => { PromisesResolver.allEvenFailed(1) }).to.throw(error);
16 | });
17 |
18 | it('should resolve all promises', (done) => {
19 | let p1Result = false,
20 | p2Result = false,
21 | p1 = new Promise((resolve, reject) => {
22 | resolve('p1');
23 | }),
24 | p2 = new Promise((resolve, reject) => {
25 | resolve('p2');
26 | });
27 |
28 | p1.then((result) => {
29 | p1Result = result;
30 | });
31 | p2.then((result) => {
32 | p2Result = result;
33 | });
34 |
35 | let result = PromisesResolver.allEvenFailed([p1, p2]);
36 |
37 | // Check that all promises were resolved
38 | result.then(() => {
39 | assert.equal(p1Result, 'p1');
40 | assert.equal(p2Result, 'p2');
41 |
42 | // assert.equal does not throw an error when failing,
43 | // so we use a callback that timeout in case of error
44 | done();
45 | });
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/tests/lib/View/MenuViewTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Entity from "../../../lib/Entity/Entity";
4 | import MenuView from "../../../lib/View/MenuView";
5 |
6 | describe('MenuView', function() {
7 |
8 | describe('disable()', () => {
9 | it('should be disabled by default', () => {
10 | const view = new MenuView();
11 | view.setEntity(new Entity('post'));
12 | assert.isFalse(view.enabled);
13 | });
14 |
15 | it('should be enabled if there\'s an enabled list view within the entity', () => {
16 | const entity = new Entity('post');
17 | entity.listView().enable();
18 | var view = new MenuView();
19 | view.setEntity(entity);
20 | assert.isTrue(view.enabled);
21 | });
22 |
23 | it('should be disabled if there\'s a disabled list view within the entity', () => {
24 | const entity = new Entity('post');
25 | entity.listView().disable();
26 | var view = new MenuView();
27 | view.setEntity(entity);
28 | assert.isFalse(view.enabled);
29 | });
30 |
31 | it('should be disabled if we disable it, even if there\'s an enabled list view within the entity', () => {
32 | const entity = new Entity('post');
33 | entity.listView().enable();
34 | var view = new MenuView()
35 | view.setEntity(entity);
36 | view.disable();
37 | assert.isFalse(view.enabled);
38 | });
39 | })
40 |
41 | describe('icon', function() {
42 | it('should default to list glyphicon', function() {
43 | var view = new MenuView(new Entity('post'));
44 | assert.equal('', view.icon());
45 | });
46 |
47 | it('should be given icon otherwise', function() {
48 | var view = new MenuView(new Entity('post')).icon('');
49 | assert.equal('', view.icon());
50 | });
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/tests/lib/DashboardTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Dashboard from "../../lib/Dashboard";
4 |
5 | describe('Dashboard', () => {
6 | describe('collections()', () => {
7 | it('should be an empty object by default', () => {
8 | assert.deepEqual(new Dashboard().collections(), {})
9 | });
10 | });
11 | describe('addCollection()', () => {
12 | it('should add a collection', () => {
13 | let dashboard = new Dashboard();
14 | const collection = { IAmAFakeCollection: true, name: () => 'foo' };
15 | dashboard.addCollection(collection);
16 | assert.deepEqual(dashboard.collections(), { foo: collection })
17 | });
18 | });
19 | describe('hasCollections()', () => {
20 | it('should return false for empty dashboards', () => {
21 | let dashboard = new Dashboard();
22 | assert.notOk(dashboard.hasCollections());
23 | });
24 | it('should return true for non-empty dashboards', () => {
25 | let dashboard = new Dashboard();
26 | dashboard.addCollection({ name: () => 'bar' });
27 | assert.ok(dashboard.hasCollections());
28 | });
29 | });
30 | describe('template()', () => {
31 | it('should return null by default', () => {
32 | let dashboard = new Dashboard();
33 | assert.isNull(dashboard.template());
34 | });
35 | it('should return the template when called with no argument', () => {
36 | let dashboard = new Dashboard();
37 | dashboard._template = 'foo';
38 | assert.equal('foo', dashboard.template());
39 | });
40 | it('should set the template when called with an argument', () => {
41 | let dashboard = new Dashboard();
42 | dashboard.template('foo');
43 | assert.equal('foo', dashboard.template());
44 | });
45 | it('should return the dashboard when called with an argument', () => {
46 | let dashboard = new Dashboard();
47 | assert.strictEqual(dashboard, dashboard.template('bar'));
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/tests/lib/Field/ReferencedListFieldTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Entry from "../../../lib/Entry";
4 | import Entity from "../../../lib/Entity/Entity";
5 | import Field from "../../../lib/Field/Field";
6 | import ReferencedListField from "../../../lib/Field/ReferencedListField";
7 | import ReferenceManyField from "../../../lib/Field/ReferenceManyField";
8 |
9 | describe('ReferencedListField', function() {
10 | it('should retrieve referenceMany fields.', function () {
11 | var entity = new Entity('comments');
12 |
13 | var referencedList = new ReferencedListField('myField'),
14 | ref1 = new ReferenceManyField('ref1'),
15 | ref2 = new ReferenceManyField('ref2');
16 |
17 | referencedList
18 | .targetEntity(entity)
19 | .targetFields([ref1, ref2]);
20 |
21 | var references = referencedList.targetFields();
22 | assert.equal(references.length, 2);
23 | assert.equal(references[0].name(), 'ref1');
24 | });
25 |
26 | it('should return information about grid column.', function () {
27 | var entity = new Entity('comments');
28 |
29 | var referencedList = new ReferencedListField('myField'),
30 | field1 = new Field('f1').label('Field 1'),
31 | field2 = new Field('f2').label('Field 2');
32 |
33 | referencedList
34 | .targetEntity(entity)
35 | .targetFields([field1, field2]);
36 |
37 | var columns = referencedList.getGridColumns();
38 |
39 | assert.equal(columns.length, 2);
40 | assert.equal(columns[0].label, 'Field 1');
41 | assert.equal(columns[1].field.name(), 'f2');
42 | });
43 |
44 | it('should store target entity configuration', function () {
45 | var entity = new Entity('comments');
46 |
47 | var post = new Entity('posts');
48 | post.editionView()
49 | .addField(new ReferencedListField('comments')
50 | .targetEntity(entity)
51 | .targetField(new Field('id'))
52 | );
53 |
54 | assert.isNotNull(post.views['EditView'].getField('comments').targetEntity().listView());
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/tests/lib/FactoryTest.js:
--------------------------------------------------------------------------------
1 | import Factory from "../../lib/Factory";
2 | import Field from "../../lib/Field/Field";
3 | import Collection from "../../lib/Collection";
4 |
5 | var assert = require('chai').assert;
6 |
7 | describe('Factory', function() {
8 | describe('field', function() {
9 | it('should return new field from given type with correct name if type is registered', function() {
10 | var factory = new Factory();
11 | class CustomField extends Field {
12 | constructor(name) {
13 | super(name);
14 | this._type = 'custom-string';
15 | }
16 | }
17 |
18 | factory.registerFieldType('custom-string', CustomField);
19 |
20 | var field = factory.field('title', 'custom-string');
21 | assert.equal('title', field.name());
22 | assert.equal('custom-string', field.type());
23 | });
24 |
25 | it('should throw an error if type has not been already registered', function() {
26 | var factory = new Factory();
27 |
28 | try {
29 | factory.field('title', 'non-existing-type');
30 | } catch(e) {
31 | assert.equal('Unknown field type "non-existing-type".', e.message);
32 | return;
33 | }
34 |
35 | assert.equal(true, false);
36 | });
37 |
38 | it('should return a string field by default', function() {
39 | var factory = new Factory();
40 | factory.registerFieldType('string', Field);
41 |
42 | var field = factory.field('title');
43 | assert.equal('string', field.type());
44 | });
45 | });
46 |
47 | describe('collection()', () => {
48 | it('should return a new Collection with the correct entity', () => {
49 | var factory = new Factory();
50 | var dummyEntity = { name: () => 'foo' };
51 | var collection = factory.collection(dummyEntity);
52 | assert.instanceOf(collection, Collection);
53 | assert.equal(collection.getEntity(), dummyEntity);
54 | assert.equal(collection.name(), 'foo');
55 | })
56 | })
57 | });
58 |
--------------------------------------------------------------------------------
/src/Field/ReferencedListField.js:
--------------------------------------------------------------------------------
1 | import ReferenceField from "./ReferenceField";
2 | import ReferenceExtractor from '../Utils/ReferenceExtractor';
3 |
4 | class ReferencedListField extends ReferenceField {
5 | constructor(name) {
6 | super(name);
7 | this._type = 'referenced_list';
8 | this._targetReferenceField = null;
9 | this._targetFields = [];
10 | this._detailLink = false;
11 | this._listActions = [];
12 | this._entryCssClasses = null;
13 | }
14 |
15 | targetReferenceField(value) {
16 | if (!arguments.length) return this._targetReferenceField;
17 | this._targetReferenceField = value;
18 | return this;
19 | }
20 |
21 | targetFields(value) {
22 | if (!arguments.length) return this._targetFields;
23 | this._targetFields = value;
24 |
25 | return this;
26 | }
27 |
28 | getGridColumns() {
29 | let columns = [];
30 | for (let i = 0, l = this._targetFields.length ; i < l ; i++) {
31 | let field = this._targetFields[i];
32 | columns.push({
33 | field: field,
34 | label: field.label()
35 | });
36 | }
37 |
38 | return columns;
39 | }
40 |
41 | getSortFieldName() {
42 | if (!this.sortField()) {
43 | return null;
44 | }
45 |
46 | return this._targetEntity.name() + '_ListView.' + this.sortField();
47 | }
48 |
49 | listActions(actions) {
50 | if (!arguments.length) {
51 | return this._listActions;
52 | }
53 |
54 | this._listActions = actions;
55 |
56 | return this;
57 | }
58 |
59 | entryCssClasses(classes) {
60 | if (!arguments.length) {
61 | return this._entryCssClasses;
62 | }
63 |
64 | this._entryCssClasses = classes;
65 |
66 | return this;
67 | }
68 |
69 | getReferences(withRemoteComplete) {
70 | return ReferenceExtractor.getReferences(this._targetFields, withRemoteComplete);
71 | }
72 |
73 | getNonOptimizedReferences(withRemoteComplete) {
74 | return ReferenceExtractor.getNonOptimizedReferences(this._targetFields, withRemoteComplete);
75 | }
76 |
77 | getOptimizedReferences(withRemoteComplete) {
78 | return ReferenceExtractor.getOptimizedReferences(this._targetFields, withRemoteComplete);
79 | }
80 | }
81 |
82 | export default ReferencedListField;
83 |
--------------------------------------------------------------------------------
/src/Entry.js:
--------------------------------------------------------------------------------
1 | import {clone, cloneAndFlatten, cloneAndNest} from './Utils/objectProperties';
2 |
3 |
4 | class Entry {
5 | constructor(entityName, values, identifierValue) {
6 | this._entityName = entityName;
7 | this.values = values || {};
8 | this._identifierValue = identifierValue;
9 | this.listValues = {};
10 | }
11 |
12 | get entityName() {
13 | return this._entityName;
14 | }
15 |
16 | get identifierValue() {
17 | return this._identifierValue;
18 | }
19 |
20 | static createForFields(fields, entityName) {
21 | let entry = new Entry(entityName);
22 | fields.forEach(field => {
23 | entry.values[field.name()] = field.defaultValue();
24 | });
25 | return entry;
26 |
27 | }
28 |
29 | /**
30 | * Map a JS object from the REST API Response to an Entry
31 | *
32 | * @return {Entry}
33 | */
34 | static createFromRest(restEntry, fields = [], entityName = '', identifierName = 'id') {
35 | if (!restEntry || Object.keys(restEntry).length == 0) {
36 | return Entry.createForFields(fields, entityName);
37 | }
38 | const excludedFields = fields.filter(f => !f.flattenable()).map(f => f.name());
39 |
40 | let values = cloneAndFlatten(restEntry, excludedFields);
41 |
42 | fields.forEach(field => {
43 | let fieldName = field.name();
44 | values[fieldName] = field.getMappedValue(values[fieldName], values);
45 | });
46 |
47 | return new Entry(entityName, values, values[identifierName]);
48 | }
49 |
50 | /**
51 | * Map an array of JS objects from the REST API Response to an array of Entries
52 | *
53 | * @return {Array[Entry]}
54 | */
55 | static createArrayFromRest(restEntries, fields, entityName, identifierName) {
56 | return restEntries.map(e => Entry.createFromRest(e, fields, entityName, identifierName));
57 | }
58 |
59 | /**
60 | * Transform an Entry to a JS object for the REST API Request
61 | *
62 | * @return {Object}
63 | */
64 | transformToRest(fields) {
65 |
66 | let restEntry = clone(this.values);
67 | fields.forEach(field => {
68 | let fieldName = field.name();
69 | if (fieldName in restEntry) {
70 | restEntry[fieldName] = field.getTransformedValue(restEntry[fieldName], restEntry)
71 | }
72 | });
73 |
74 | return cloneAndNest(restEntry);
75 | }
76 |
77 | }
78 |
79 | export default Entry;
80 |
--------------------------------------------------------------------------------
/src/Queries/WriteQueries.js:
--------------------------------------------------------------------------------
1 | import Queries from './Queries'
2 |
3 | class WriteQueries extends Queries {
4 |
5 | /**
6 | * Create a new entity
7 | * Post the data to the API to create the new object
8 | *
9 | * @param {View} view the formView related to the entity
10 | * @param {Object} rawEntity the entity's object
11 | *
12 | * @returns {promise} the new object
13 | */
14 | createOne(view, rawEntity) {
15 | return this._restWrapper
16 | .createOne(rawEntity, view.entity.name(), this._application.getRouteFor(view.entity, view.getUrl(), view.type), view.entity.createMethod());
17 | }
18 |
19 | /**
20 | * Update an entity
21 | * Put the data to the API to create the new object
22 | *
23 | * @param {View} view the formView related to the entity
24 | * @param {Object} rawEntity the entity's object
25 | * @param {String} originEntityId if entity identifier is modified
26 | *
27 | * @returns {promise} the updated object
28 | */
29 | updateOne(view, rawEntity, originEntityId) {
30 | let entityId = originEntityId || rawEntity[view.entity.identifier().name()];
31 |
32 | // Update element data
33 | return this._restWrapper
34 | .updateOne(rawEntity, view.entity.name(), this._application.getRouteFor(view.entity, view.getUrl(entityId), view.type, entityId, view.identifier()), view.entity.updateMethod());
35 | }
36 |
37 | /**
38 | * Delete an entity
39 | * Delete the data to the API
40 | *
41 | * @param {String} view the formView related to the entity
42 | * @param {*} entityId the entity's id
43 | *
44 | * @returns {promise}
45 | */
46 | deleteOne(view, entityId) {
47 | return this._restWrapper
48 | .deleteOne(view.entity.name(), this._application.getRouteFor(view.entity, view.getUrl(entityId), view.type, entityId, view.identifier()), view.entity.deleteMethod());
49 | }
50 |
51 | /**
52 | * Delete a batch of entity
53 | * Delete the data to the API
54 | *
55 | * @param {String} view the formView related to the entity
56 | * @param {*} entityIds the entities ids
57 | *
58 | * @returns {promise}
59 | */
60 | batchDelete(view, entityIds) {
61 | let deleteOne = this.deleteOne.bind(this)
62 | let promises = entityIds.map(function (id) {
63 | return deleteOne(view, id);
64 | });
65 |
66 | return this._promisesResolver.allEvenFailed(promises);
67 | }
68 | }
69 |
70 | export default WriteQueries
71 |
--------------------------------------------------------------------------------
/tests/lib/Field/ReferenceFieldTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Entry from "../../../lib/Entry";
4 | import Entity from "../../../lib/Entity/Entity";
5 | import Field from "../../../lib/Field/Field";
6 | import ReferenceField from "../../../lib/Field/ReferenceField";
7 |
8 | describe('ReferenceField', function() {
9 | describe('detailLink', function() {
10 | it('should be a detail link by default', function() {
11 | var field = new ReferenceField('foo');
12 | assert.equal(true, field.isDetailLink());
13 | });
14 | });
15 |
16 | it('Should create a fake view to keep entity', function () {
17 | var post = new Entity('posts'),
18 | comment = new Entity('comments');
19 |
20 | comment.listView()
21 | .addField(new ReferenceField('post_id')
22 | .targetEntity(post)
23 | .targetField(new Field('id'))
24 | );
25 |
26 | var fieldName = comment.views["ListView"].getField('post_id').targetEntity().name();
27 | assert.equal(fieldName, 'posts');
28 | });
29 |
30 | describe('getSortFieldName', function () {
31 | it('should retrieve sortField', function () {
32 | var ref = new ReferenceField('human_id'),
33 | human = new Entity('human');
34 |
35 | ref.entries = [
36 | new Entry({ id: 1, human_id: 1, name: 'Suna'}),
37 | new Entry({ id: 2, human_id: 2, name: 'Boby'}),
38 | new Entry({ id: 3, human_id: 1, name: 'Mizute'})
39 | ];
40 |
41 | ref
42 | .targetEntity(human)
43 | .targetField(new Field('name'))
44 | .sortField('name')
45 | .sortDir('DESC');
46 |
47 | human
48 | .identifier(new Field('id'))
49 | .editionView();
50 |
51 | assert.equal(ref.getSortFieldName(), 'human_ListView.name');
52 | });
53 | });
54 |
55 | describe('getIdentifierValues', function () {
56 | it('Should return identifier values', function () {
57 | var view = new ReferenceField('tags'),
58 | identifiers;
59 |
60 | identifiers = view.getIdentifierValues([{_id: 1, tags:[1, 3]}, {_id:3, id:6, tags:[4, 3]}]);
61 | assert.deepEqual(identifiers, ['1', '3', '4']);
62 | });
63 |
64 | it('Should not return undefined values', function () {
65 | var view = new ReferenceField('tags'),
66 | identifiers;
67 |
68 | identifiers = view.getIdentifierValues([{_id: 1, tags:undefined}, {_id:3, id:6, tags:[3]}]);
69 | assert.deepEqual(identifiers, ['3']);
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/tests/lib/Utils/ReferenceExtractorTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import ReferenceExtractor from "../../../lib/Utils/ReferenceExtractor";
4 |
5 | describe('ReferenceExtractor', () => {
6 |
7 | describe('getReferences', () => {
8 | it('should return an empty object of empty field array', () => {
9 | assert.deepEqual({}, ReferenceExtractor.getReferences([]));
10 | });
11 |
12 | it('should index by reference name', () => {
13 | const fields = [
14 | { type() { return 'reference' }, name() { return 'foo' } },
15 | { type() { return 'reference' }, name() { return 'bar' } }
16 | ];
17 | assert.deepEqual({
18 | foo: fields[0],
19 | bar: fields[1]
20 | }, ReferenceExtractor.getReferences(fields));
21 | })
22 |
23 | it('should filter out non-reference fields', () => {
24 | const fields = [
25 | { type() { return 'reference' }, name() { return 'foo' } },
26 | { type() { return 'reference_many' }, name() { return 'bar' } },
27 | { type() { return 'referenced_list' }, name() { return 'baz' } },
28 | { type() { return 'string' }, name() { return 'boo' } }
29 | ];
30 | assert.deepEqual({
31 | foo: fields[0],
32 | bar: fields[1]
33 | }, ReferenceExtractor.getReferences(fields));
34 | })
35 | })
36 |
37 | describe('getReferencedLists', () => {
38 | it('should return an empty object of empty field array', () => {
39 | assert.deepEqual({}, ReferenceExtractor.getReferencedLists([]));
40 | });
41 |
42 | it('should index by reference name', () => {
43 | const fields = [
44 | { type() { return 'referenced_list' }, name() { return 'foo' } },
45 | { type() { return 'referenced_list' }, name() { return 'bar' } }
46 | ];
47 | assert.deepEqual({
48 | foo: fields[0],
49 | bar: fields[1]
50 | }, ReferenceExtractor.getReferencedLists(fields));
51 | })
52 |
53 | it('should filter out non-referenced_list fields', () => {
54 | const fields = [
55 | { type() { return 'reference' }, name() { return 'foo' } },
56 | { type() { return 'reference_many' }, name() { return 'bar' } },
57 | { type() { return 'referenced_list' }, name() { return 'baz' } },
58 | { type() { return 'string' }, name() { return 'boo' } }
59 | ];
60 | assert.deepEqual({
61 | baz: fields[2]
62 | }, ReferenceExtractor.getReferencedLists(fields));
63 | })
64 | })
65 |
66 | });
67 |
--------------------------------------------------------------------------------
/tests/lib/Utils/objectPropertiesTest.js:
--------------------------------------------------------------------------------
1 | const assert = require('chai').assert;
2 |
3 | import {cloneAndFlatten, cloneAndNest} from '../../../lib/Utils/objectProperties';
4 |
5 | describe('cloneAndFlatten()', () => {
6 | it('should not allow a non-object parameter', () =>
7 | assert.throws(() => cloneAndFlatten(2))
8 | );
9 | it('should return same values for flat objects', () =>
10 | assert.deepEqual(cloneAndFlatten({ foo: 1, bar: 'baz'}), { foo: 1, bar: 'baz'})
11 | );
12 | it('should copy null values', () =>
13 | assert.deepEqual(cloneAndFlatten({ foo: null }), { foo: null })
14 | );
15 | it('should clone the input', () => {
16 | let object = { foo: 1, bar: 'baz'};
17 | let flatObject = cloneAndFlatten(object);
18 | assert.notStrictEqual(flatObject, object);
19 | flatObject.foo = 2;
20 | assert.equal(object.foo, 1);
21 | });
22 | it('should flatten nested objects', () =>
23 | assert.deepEqual(cloneAndFlatten({ a: 1, b: { c: 2 }, d: { e: 3, f: { g: 4, h: 5 } } }),
24 | { a: 1, 'b.c': 2, 'd.e': 3, 'd.f.g': 4, 'd.f.h': 5 })
25 | );
26 | it('should not flatten arrays', () =>
27 | assert.deepEqual(cloneAndFlatten({ a: [1, 2, 3] }), { a: [1, 2, 3]})
28 | );
29 | it('should not flatten strings', () =>
30 | assert.deepEqual(cloneAndFlatten({ a: "hello, world" }), { a: "hello, world" })
31 | );
32 | it('should not flatten dates', () => {
33 | let d = new Date();
34 | assert.deepEqual(cloneAndFlatten({ a: d }), { a: d })
35 | });
36 | it('should not flatten excluded properties', () =>
37 | assert.deepEqual(cloneAndFlatten({ a: 1, b: { c: 2 }, d: { e: 3, f: { g: 4, h: 5 } } }, ['d']),
38 | { a: 1, 'b.c': 2, d: { e: 3, f: { g: 4, h: 5 } } })
39 | );
40 | });
41 |
42 | describe('cloneAndNest()', () => {
43 | it('should not allow a non-object parameter', () =>
44 | assert.throws(() => cloneAndNest(2))
45 | );
46 | it('should return same values for flat objects', () =>
47 | assert.deepEqual(cloneAndNest({ foo: 1, bar: 'baz'}), { foo: 1, bar: 'baz'})
48 | );
49 | it('should copy null values', () =>
50 | assert.deepEqual(cloneAndNest({ foo: null }), { foo: null })
51 | );
52 | it('should clone the input', () => {
53 | let object = { foo: 1, bar: 'baz'};
54 | let nestedObject = cloneAndNest(object);
55 | assert.notStrictEqual(nestedObject, object);
56 | nestedObject.foo = 2;
57 | assert.equal(object.foo, 1);
58 | });
59 | it('should nest flattened objects', () =>
60 | assert.deepEqual(cloneAndNest({ a: 1, 'b.c': 2, 'd.e': 3, 'd.f.g': 4, 'd.f.h': 5 }),
61 | { a: 1, b: { c: 2 }, d: { e: 3, f: { g: 4, h: 5 } } })
62 | );
63 | it('should not error on null nested objects', () =>
64 | assert.deepEqual(cloneAndNest({ a: null, 'a.b': null }),
65 | { a: null })
66 | );
67 | });
68 |
--------------------------------------------------------------------------------
/src/Utils/objectProperties.js:
--------------------------------------------------------------------------------
1 | function isObject(value) {
2 | if (value === null) return false;
3 | if (typeof value !== 'object') return false;
4 | if (Array.isArray(value)) return false;
5 | if (Object.prototype.toString.call(value) === '[object Date]') return false;
6 | return true;
7 | }
8 |
9 | export function clone(object) {
10 | return Object.keys(object).reduce((values, name) => {
11 | if (object.hasOwnProperty(name)) {
12 | values[name] = object[name];
13 | }
14 | return values;
15 | }, {});
16 | }
17 |
18 | /*
19 | * Flatten nested object into a single level object with 'foo.bar' property names
20 | *
21 | * The parameter object is left unchanged. All values in the returned object are scalar.
22 | *
23 | * cloneAndFlatten({ a: 1, b: { c: 2 }, d: { e: 3, f: { g: 4, h: 5 } }, i: { j: 6 } }, ['i'])
24 | * // { a: 1, 'b.c': 2, 'd.e': 3, 'd.f.g': 4, 'd.f.h': 5, i: { j: 6 } } }
25 | *
26 | * @param {Object} object
27 | * @param {String[]} excludedProperties
28 | * @return {Object}
29 | */
30 | export function cloneAndFlatten(object, excludedProperties = []) {
31 | if (typeof object !== 'object') {
32 | throw new Error('Expecting an object parameter');
33 | }
34 | return Object.keys(object).reduce((values, name) => {
35 | if (!object.hasOwnProperty(name)) return values;
36 | if (isObject(object[name])) {
37 | if (excludedProperties.indexOf(name) === -1) {
38 | let flatObject = cloneAndFlatten(object[name]);
39 | Object.keys(flatObject).forEach(flatObjectKey => {
40 | if (!flatObject.hasOwnProperty(flatObjectKey)) return;
41 | values[name + '.' + flatObjectKey] = flatObject[flatObjectKey];
42 | })
43 | } else {
44 | values[name] = clone(object[name]);
45 | }
46 | } else {
47 | values[name] = object[name];
48 | }
49 | return values;
50 | }, {});
51 | };
52 |
53 | /*
54 | * Clone flattened object into a nested object
55 | *
56 | * The parameter object is left unchanged.
57 | *
58 | * cloneAndNest({ a: 1, 'b.c': 2, 'd.e': 3, 'd.f.g': 4, 'd.f.h': 5 } )
59 | * // { a: 1, b: { c: 2 }, d: { e: 3, f: { g: 4, h: 5 } } }
60 | *
61 | * @param {Object} object
62 | * @return {Object}
63 | */
64 | export function cloneAndNest(object) {
65 | if (typeof object !== 'object') {
66 | throw new Error('Expecting an object parameter');
67 | }
68 | return Object.keys(object).reduce((values, name) => {
69 | if (!object.hasOwnProperty(name)) return values;
70 | name.split('.').reduce((previous, current, index, list) => {
71 | if (previous != null) {
72 | if (typeof previous[current] === 'undefined') previous[current] = {};
73 | if (index < (list.length - 1)) {
74 | return previous[current];
75 | };
76 | previous[current] = object[name];
77 | }
78 | }, values)
79 | return values;
80 | }, {})
81 | }
82 |
--------------------------------------------------------------------------------
/src/DataStore/DataStore.js:
--------------------------------------------------------------------------------
1 | class DataStore {
2 | constructor() {
3 | this._entries = {};
4 | }
5 |
6 | setEntries(name, entries) {
7 | this._entries[name] = entries;
8 |
9 | return this;
10 | }
11 |
12 | addEntry(name, entry) {
13 | if (!(name in this._entries)) {
14 | this._entries[name] = [];
15 | }
16 |
17 | this._entries[name].push(entry);
18 | }
19 |
20 | getEntries(name) {
21 | return this._entries[name] || [];
22 | }
23 |
24 | /**
25 | * Get first entry satisfying a filter function
26 | *
27 | * @example datastore.getEntry('books', book => book.title === 'War and Peace');
28 | */
29 | getFirstEntry(name, filter = () => true) {
30 | return this.getEntries(name)
31 | .filter(filter)
32 | .shift();
33 | }
34 |
35 | getChoices(field) {
36 | let identifier = field.targetEntity().identifier().name();
37 | let name = field.targetField().name();
38 |
39 | return this.getEntries(field.targetEntity().uniqueId + '_choices').map(function(entry) {
40 | return {
41 | value: entry.values[identifier],
42 | label: entry.values[name]
43 | };
44 | });
45 | }
46 |
47 | fillReferencesValuesFromCollection(collection, referencedValues, fillSimpleReference) {
48 | fillSimpleReference = typeof (fillSimpleReference) === 'undefined' ? false : fillSimpleReference;
49 |
50 | for (let i = 0, l = collection.length; i < l; i++) {
51 | collection[i] = this.fillReferencesValuesFromEntry(collection[i], referencedValues, fillSimpleReference);
52 | }
53 |
54 | return collection;
55 | }
56 |
57 | fillReferencesValuesFromEntry(entry, referencedValues, fillSimpleReference) {
58 | for (let referenceField in referencedValues) {
59 | let reference = referencedValues[referenceField],
60 | choices = this.getReferenceChoicesById(reference),
61 | entries = [],
62 | identifier = reference.getMappedValue(entry.values[referenceField], entry.values);
63 |
64 | if (reference.type() === 'reference_many') {
65 | for (let i in identifier) {
66 | let id = identifier[i];
67 | entries.push(choices[id]);
68 | }
69 |
70 | entry.listValues[referenceField] = entries;
71 | } else if (fillSimpleReference && identifier != null && identifier in choices) {
72 | entry.listValues[referenceField] = reference.getMappedValue(choices[identifier], entry.values);
73 | }
74 | }
75 |
76 | return entry;
77 | }
78 |
79 | getReferenceChoicesById(field) {
80 | let result = {},
81 | targetField = field.targetField().name(),
82 | targetIdentifier = field.targetEntity().identifier().name(),
83 | entries = this.getEntries(field.targetEntity().uniqueId + '_values');
84 |
85 | for (let i = 0, l = entries.length ; i < l ; i++) {
86 | let entry = entries[i];
87 | result[entry.values[targetIdentifier]] = entry.values[targetField];
88 | }
89 |
90 | return result;
91 | }
92 | }
93 |
94 | export default DataStore;
95 |
--------------------------------------------------------------------------------
/src/Field/EmbeddedListField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 | import Entity from "../Entity/Entity";
3 |
4 | /**
5 | * Map an embedded list in the entry
6 | *
7 | * @example
8 | *
9 | * {
10 | * id: 123,
11 | * title: "hello, world",
12 | * comments: [
13 | * { date: "2015-09-30", author: "John Doe", body: "Lorem Ipsum" },
14 | * { date: "2015-10-02", author: "Jane Doe", body: "Sic dolor amet" }
15 | * ]
16 | * }
17 | *
18 | * let commentsField = new EmbeddedListField('comments')
19 | * .targetFields([
20 | * new DateField('date'),
21 | * new StringField('author'),
22 | * new StringField('body')
23 | * ])
24 | */
25 | class EmbeddedListField extends Field {
26 | constructor(name) {
27 | super(name);
28 | this._type = 'embedded_list';
29 | this._flattenable = false;
30 | this._targetEntity = new Entity(); // link to an empty entity by default
31 | this._targetFields = [];
32 | this._sortField = null;
33 | this._sortDir = null;
34 | this._permanentFilters = null;
35 | this._listActions = [];
36 | }
37 |
38 | /**
39 | * Optionally set the target Entity
40 | *
41 | * Useful if the embedded entries can be edited in standalone
42 | */
43 | targetEntity(entity) {
44 | if (!arguments.length) {
45 | return this._targetEntity;
46 | }
47 | this._targetEntity = entity;
48 |
49 | return this;
50 | }
51 |
52 | /**
53 | * List the fields to map in the embedded entries
54 | *
55 | * @example
56 | *
57 | * embeddedListField.targetFields([
58 | * new DateField('date'),
59 | * new StringField('author'),
60 | * new StringField('body')
61 | * ])
62 | */
63 | targetFields(value) {
64 | if (!arguments.length) return this._targetFields;
65 | this._targetFields = value;
66 |
67 | return this;
68 | }
69 |
70 | /**
71 | * Name of the field used for sorting.
72 | *
73 | * @param string
74 | */
75 | sortField() {
76 | if (arguments.length) {
77 | this._sortField = arguments[0];
78 | return this;
79 | }
80 |
81 | return this._sortField ? this._sortField : this.targetEntity().identifier().name();
82 | }
83 |
84 | /**
85 | * Direction used for sorting.
86 | *
87 | * @param String either 'ASC' or 'DESC'
88 | */
89 | sortDir() {
90 | if (arguments.length) {
91 | this._sortDir = arguments[0];
92 | return this;
93 | }
94 |
95 | return this._sortDir;
96 | }
97 |
98 | listActions(actions) {
99 | if (!arguments.length) {
100 | return this._listActions;
101 | }
102 |
103 | this._listActions = actions;
104 |
105 | return this;
106 | }
107 |
108 | /**
109 | * Define permanent filters to be added to the REST API calls
110 | *
111 | * nga.field('post_id', 'reference').permanentFilters({
112 | * published: true
113 | * });
114 | * // related API call will be /posts/:id?published=true
115 | *
116 | * @param {Object} filters list of filters to apply to the call
117 | */
118 | permanentFilters(filters) {
119 | if (!arguments.length) {
120 | return this._permanentFilters;
121 | }
122 |
123 | this._permanentFilters = filters;
124 |
125 | return this;
126 | }
127 | }
128 |
129 | export default EmbeddedListField;
130 |
--------------------------------------------------------------------------------
/src/Menu/Menu.js:
--------------------------------------------------------------------------------
1 | import Entity from '../Entity/Entity'
2 |
3 | function alwaysFalse() {
4 | return false;
5 | }
6 |
7 | var uuid = 0;
8 | var autoClose = true;
9 |
10 | class Menu {
11 | constructor() {
12 | this._link = null;
13 | this._activeFunc = alwaysFalse;
14 | this._title = null;
15 | this._icon = false;
16 | this._children = [];
17 | this._template = false;
18 | this._autoClose = true;
19 | this.uuid = uuid++;
20 | }
21 |
22 | title() {
23 | if (arguments.length) {
24 | this._title = arguments[0];
25 | return this;
26 | }
27 | return this._title;
28 | }
29 |
30 | isLink() {
31 | return !!this._link;
32 | }
33 |
34 | link() {
35 | if (arguments.length) {
36 | this._link = arguments[0];
37 | if (this._activeFunc == alwaysFalse) {
38 | this._activeFunc = url => url.indexOf(this._link) === 0;
39 | }
40 | return this;
41 | }
42 | return this._link;
43 | }
44 |
45 | autoClose() {
46 | if (arguments.length) {
47 | autoClose = arguments[0];
48 | return this;
49 | }
50 | return autoClose;
51 | }
52 |
53 | active(activeFunc) {
54 | if (arguments.length) {
55 | this._activeFunc = arguments[0];
56 | return this;
57 | }
58 | return this._activeFunc;
59 | }
60 |
61 | isActive(url) {
62 | return this._activeFunc(url);
63 | }
64 |
65 | isChildActive(url) {
66 | return this.isActive(url) || (this.children().filter(menu => menu.isChildActive(url)).length > 0);
67 | }
68 |
69 | addChild(child) {
70 | if (!(child instanceof Menu)) {
71 | throw new Error('Only Menu instances are accepted as children of a Menu');
72 | }
73 | this._children.push(child);
74 | return this;
75 | }
76 |
77 | hasChild() {
78 | return this._children.length > 0;
79 | }
80 |
81 | getChildByTitle(title) {
82 | return this.children().filter(child => child.title() == title).pop();
83 | }
84 |
85 | children() {
86 | if (arguments.length) {
87 | this._children = arguments[0];
88 | return this;
89 | }
90 | return this._children;
91 | }
92 |
93 | icon() {
94 | if (arguments.length) {
95 | this._icon = arguments[0];
96 | return this;
97 | }
98 | return this._icon;
99 | }
100 |
101 | template() {
102 | if (arguments.length) {
103 | this._template = arguments[0];
104 | return this;
105 | }
106 | return this._template;
107 | }
108 |
109 | populateFromEntity(entity) {
110 | if (!(entity instanceof Entity)) {
111 | throw new Error('populateFromEntity() only accepts an Entity parameter');
112 | }
113 | this.title(entity.label());
114 | this.active(path => path.indexOf(`/${entity.name()}/`) === 0 );
115 |
116 | let search = "";
117 | const defaultFilters = entity.listView().filters()
118 | .filter(filter => filter.pinned() && filter.defaultValue())
119 | .reduce((filters, currentFilter) => Object.assign(
120 | {},
121 | filters,
122 | {
123 | [currentFilter.name()] : currentFilter.getTransformedValue(currentFilter.defaultValue())
124 | }
125 | ), {});
126 |
127 | if(Object.keys(defaultFilters).length){
128 | const encodedDefaultFilters = encodeURIComponent(JSON.stringify(defaultFilters));
129 | search = `?search=${encodedDefaultFilters}`
130 | }
131 |
132 | this.link(`/${entity.name()}/list${search}`);
133 | // deprecated
134 | this.icon(entity.menuView().icon());
135 | return this;
136 | }
137 | }
138 |
139 | export default Menu;
140 |
--------------------------------------------------------------------------------
/tests/lib/Queries/WriteQueriesTest.js:
--------------------------------------------------------------------------------
1 | let assert = require('chai').assert,
2 | sinon = require('sinon');
3 |
4 | import WriteQueries from "../../../lib/Queries/WriteQueries";
5 | import DataStore from "../../../lib/DataStore/DataStore";
6 | import PromisesResolver from "../../mock/PromisesResolver";
7 | import Entity from "../../../lib/Entity/Entity";
8 | import TextField from "../../../lib/Field/TextField";
9 | import buildPromise from "../../mock/mixins";
10 |
11 | describe('WriteQueries', () => {
12 | let writeQueries,
13 | restWrapper = {},
14 | application = {},
15 | entity,
16 | view;
17 |
18 | beforeEach(() => {
19 | application = {
20 | getRouteFor: (entity, generatedUrl, viewType, id) => {
21 | let url = 'http://localhost/' + encodeURIComponent(entity.name());
22 | if (id) {
23 | url += '/' + encodeURIComponent(id);
24 | }
25 |
26 | return url;
27 | }
28 | };
29 |
30 | writeQueries = new WriteQueries(restWrapper, PromisesResolver, application);
31 | entity = new Entity('cat');
32 | view = entity.views["CreateView"]
33 | .addField(new TextField('name'));
34 | });
35 |
36 | describe('createOne', () => {
37 | it('should POST an entity when calling createOne', () => {
38 | let rawEntity = {name: 'Mizu'};
39 |
40 | restWrapper.createOne = sinon.stub().returns(buildPromise({data: rawEntity}));
41 |
42 | writeQueries.createOne(view, rawEntity)
43 | .then(rawEntry => {
44 | assert(restWrapper.createOne.calledWith(rawEntity, 'cat', 'http://localhost/cat'));
45 |
46 | let dataStore = new DataStore();
47 | let entry = view.mapEntry(rawEntry);
48 | assert.equal(entry.values['data.name'], 'Mizu');
49 | });
50 | });
51 | });
52 |
53 | describe('updateOne', () => {
54 | let rawEntity = {id: 3, name: 'Mizu'},
55 | updatedEntity = {id: 3, name: 'Mizute'};
56 |
57 | restWrapper.updateOne = sinon.stub().returns(buildPromise({data: updatedEntity}));
58 |
59 | it('should PUT an entity when calling updateOne', () => {
60 | writeQueries.updateOne(view, rawEntity)
61 | .then(rawEntry => {
62 | assert(restWrapper.updateOne.calledWith(rawEntity, 'cat', 'http://localhost/cat/3'));
63 |
64 | let dataStore = new DataStore();
65 | let entry = view.mapEntry(rawEntry);
66 | assert.equal(entry.values['data.name'], 'Mizute');
67 | });
68 | });
69 |
70 | it('should PUT an entity when calling updateOne with an id', () => {
71 | writeQueries.updateOne(view, rawEntity, 3)
72 | .then(rawEntry => {
73 | assert(restWrapper.updateOne.calledWith(rawEntity, 'cat', 'http://localhost/cat/3'));
74 |
75 | let dataStore = new DataStore();
76 | let entry = view.mapEntry(rawEntry);
77 | assert.equal(entry.values['data.name'], 'Mizute');
78 | });
79 | });
80 | });
81 |
82 | describe("deleteOne", () => {
83 | restWrapper.deleteOne = sinon.stub().returns(buildPromise({}));
84 |
85 | it('should DELETE an entity when calling deleteOne', () => {
86 | writeQueries.deleteOne(view, 1)
87 | .then(() => {
88 | assert(restWrapper.deleteOne.calledWith('cat', 'http://localhost/cat/1'));
89 | });
90 | });
91 | });
92 |
93 | describe("batchDelete", () => {
94 | it('should DELETE entities when calling batchEntities', () => {
95 | restWrapper.deleteOne = sinon.stub().returns(buildPromise({}));
96 |
97 | writeQueries.batchDelete(view, [1, 2])
98 | .then(() => {
99 | assert(restWrapper.deleteOne.calledTwice);
100 | assert(restWrapper.deleteOne.calledWith('cat', 'http://localhost/cat/1'));
101 | assert(restWrapper.deleteOne.calledWith('cat', 'http://localhost/cat/2'));
102 | });
103 | });
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/src/Factory.js:
--------------------------------------------------------------------------------
1 | import Application from "./Application";
2 | import Entity from "./Entity/Entity";
3 | import DataStore from "./DataStore/DataStore";
4 | import PromisesResolver from "./Utils/PromisesResolver";
5 |
6 | import ReadQueries from "./Queries/ReadQueries";
7 | import WriteQueries from "./Queries/WriteQueries";
8 |
9 | import Field from "./Field/Field";
10 | import BooleanField from "./Field/BooleanField";
11 | import ChoiceField from "./Field/ChoiceField";
12 | import ChoicesField from "./Field/ChoicesField";
13 | import DateField from "./Field/DateField";
14 | import DateTimeField from "./Field/DateTimeField";
15 | import EmailField from "./Field/EmailField";
16 | import EmbeddedListField from "./Field/EmbeddedListField";
17 | import FloatField from "./Field/FloatField.js";
18 | import FileField from "./Field/FileField";
19 | import JsonField from "./Field/JsonField";
20 | import NumberField from "./Field/NumberField";
21 | import PasswordField from "./Field/PasswordField";
22 | import ReferenceField from "./Field/ReferenceField";
23 | import ReferencedListField from "./Field/ReferencedListField";
24 | import ReferenceManyField from "./Field/ReferenceManyField";
25 | import TemplateField from "./Field/TemplateField";
26 | import TextField from "./Field/TextField";
27 | import WysiwygField from "./Field/WysiwygField";
28 |
29 | import Menu from './Menu/Menu';
30 | import Collection from './Collection';
31 | import Dashboard from './Dashboard';
32 | import Entry from './Entry';
33 |
34 | class Factory {
35 | constructor() {
36 | this._fieldTypes = [];
37 | this._init();
38 | }
39 |
40 | application(name, debug) {
41 | return new Application(name, debug);
42 | }
43 |
44 | entity(name) {
45 | return new Entity(name);
46 | }
47 |
48 | field(name, type) {
49 | type = type || 'string';
50 |
51 | if (!(type in this._fieldTypes)) {
52 | throw new Error(`Unknown field type "${type}".`);
53 | }
54 |
55 | return new this._fieldTypes[type](name);
56 | }
57 |
58 | registerFieldType(name, constructor) {
59 | this._fieldTypes[name] = constructor;
60 | }
61 |
62 | getFieldConstructor(name) {
63 | return this._fieldTypes[name];
64 | }
65 |
66 | menu(entity) {
67 | let menu = new Menu();
68 | if (entity) {
69 | menu.populateFromEntity(entity);
70 | }
71 | return menu;
72 | }
73 |
74 | dashboard() {
75 | return new Dashboard();
76 | }
77 |
78 | collection(entity) {
79 | let collection = new Collection();
80 | if (entity) {
81 | collection.setEntity(entity);
82 | }
83 | return collection;
84 | }
85 |
86 | getEntryConstructor() {
87 | return Entry;
88 | }
89 |
90 | getDataStore() {
91 | return new DataStore();
92 | }
93 |
94 | getReadQueries(RestWrapper, PromisesResolver, Application) {
95 | return new ReadQueries(RestWrapper, PromisesResolver, Application);
96 | }
97 |
98 | getWriteQueries(RestWrapper, PromisesResolver, Application) {
99 | return new WriteQueries(RestWrapper, PromisesResolver, Application);
100 | }
101 |
102 | getPromisesResolver() {
103 | return PromisesResolver;
104 | }
105 |
106 | _init() {
107 | this.registerFieldType('boolean', BooleanField);
108 | this.registerFieldType('choice', ChoiceField);
109 | this.registerFieldType('choices', ChoicesField);
110 | this.registerFieldType('date', DateField);
111 | this.registerFieldType('datetime', DateTimeField);
112 | this.registerFieldType('email', EmailField);
113 | this.registerFieldType('embedded_list', EmbeddedListField);
114 | this.registerFieldType('float', FloatField);
115 | this.registerFieldType('string', Field);
116 | this.registerFieldType('file', FileField);
117 | this.registerFieldType('json', JsonField);
118 | this.registerFieldType('number', NumberField);
119 | this.registerFieldType('password', PasswordField);
120 | this.registerFieldType('reference', ReferenceField);
121 | this.registerFieldType('reference_many', ReferenceManyField);
122 | this.registerFieldType('referenced_list', ReferencedListField);
123 | this.registerFieldType('template', TemplateField);
124 | this.registerFieldType('text', TextField);
125 | this.registerFieldType('wysiwyg', WysiwygField);
126 | }
127 | }
128 |
129 | export default Factory;
130 |
--------------------------------------------------------------------------------
/src/View/DeleteView.js:
--------------------------------------------------------------------------------
1 | import View from './View';
2 |
3 | class DeleteView extends View {
4 | constructor(name) {
5 | super(name);
6 | this._type = 'DeleteView';
7 | this._enabled = true;
8 | this._submitCreationSuccess = null;
9 | this._submitCreationError = null;
10 | }
11 |
12 | /**
13 | * Add a function to be executed after the delete succeeds.
14 | *
15 | * This is the ideal place to use the response to delete the entry, or
16 | * redirect to another view.
17 | *
18 | * If the function returns false, the default execution workflow is stopped.
19 | * This means that the function must provide a custom workflow.
20 | *
21 | * If the function throws an exception, the onSubmitError callback will
22 | * execute.
23 | *
24 | * The syntax depends on the framework calling the function.
25 | *
26 | * With ng-admin, the function can be an angular injectable, listing
27 | * required dependencies in an array. Among other, the function can receive
28 | * the following services:
29 | * - $event: the form submission event
30 | * - entry: the current Entry instance
31 | * - entity: the current entity
32 | * - form: the form object (for form validation and errors)
33 | * - progression: the controller for the loading indicator
34 | * - notification: the controller for top notifications
35 | *
36 | * The function can be asynchronous, in which case it should return
37 | * a Promise.
38 | *
39 | * @example
40 | *
41 | * post.deletionView().onSubmitSuccess(['progression', 'notification', '$state', 'entry', 'entity', function(progression, notification, $state, entry, entity) {
42 | * // stop the progress bar
43 | * progression.done();
44 | * // add a notification
45 | * notification.log(`Element #${entry._identifierValue} successfully deleted.`, { addnCls: 'humane-flatty-success' });
46 | * // redirect to the list view
47 | * $state.go($state.get('list'), { entity: entity.name() });
48 | * // cancel the default action (redirect to the edition view)
49 | * return false;
50 | * }])
51 | */
52 | onSubmitSuccess(onSubmitSuccess) {
53 | if (!arguments.length) return this._onSubmitSuccess;
54 | this._onSubmitSuccess = onSubmitSuccess;
55 | return this;
56 | }
57 |
58 | /**
59 | * Add a function to be executed after the delete request receives a failed
60 | * http response from the server.
61 | *
62 | * This is the ideal place to use the response to delete the entry, display
63 | * server-side validation error, or redirect to another view.
64 | *
65 | * If the function returns false, the default execution workflow is stopped.
66 | * This means that the function must provide a custom workflow.
67 | *
68 | * The syntax depends on the framework calling the function.
69 | *
70 | * With ng-admin, the function can be an angular injectable, listing
71 | * required dependencies in an array. Among other, the function can receive
72 | * the following services:
73 | * - $event: the form submission event
74 | * - error: the response from the server
75 | * - errorMessage: the error message based on the response
76 | * - entry: the current Entry instance
77 | * - entity: the current entity
78 | * - form: the form object (for form validation and errors)
79 | * - progression: the controller for the loading indicator
80 | * - notification: the controller for top notifications
81 | *
82 | * The function can be asynchronous, in which case it should return
83 | * a Promise.
84 | *
85 | * @example
86 | *
87 | * post.deletionView().onSubmitError(['error', 'form', 'progression', 'notification', function(error, form, progression, notification) {
88 | * // stop the progress bar
89 | * progression.done();
90 | * // add a notification
91 | * notification.log(`Failed to delete element.`, { addnCls: 'humane-flatty-error' });
92 | * // cancel the default action (default error messages)
93 | * return false;
94 | * }]);
95 | */
96 | onSubmitError(onSubmitError) {
97 | if (!arguments.length) return this._onSubmitError;
98 | this._onSubmitError = onSubmitError;
99 | return this;
100 | }
101 | }
102 |
103 | export default DeleteView;
104 |
--------------------------------------------------------------------------------
/src/View/EditView.js:
--------------------------------------------------------------------------------
1 | import View from './View';
2 |
3 | class EditView extends View {
4 | constructor(name) {
5 | super(name);
6 | this._type = 'EditView';
7 | this._submitCreationSuccess = null;
8 | this._submitCreationError = null;
9 | }
10 |
11 | /**
12 | * Add a function to be executed after the update succeeds.
13 | *
14 | * This is the ideal place to use the response to update the entry, or
15 | * redirect to another view.
16 | *
17 | * If the function returns false, the default execution workflow is stopped.
18 | * This means that the function must provide a custom workflow.
19 | *
20 | * If the function throws an exception, the onSubmitError callback will
21 | * execute.
22 | *
23 | * The syntax depends on the framework calling the function.
24 | *
25 | * With ng-admin, the function can be an angular injectable, listing
26 | * required dependencies in an array. Among other, the function can receive
27 | * the following services:
28 | * - $event: the form submission event
29 | * - entry: the current Entry instance
30 | * - entity: the current entity
31 | * - form: the form object (for form validation and errors)
32 | * - progression: the controller for the loading indicator
33 | * - notification: the controller for top notifications
34 | *
35 | * The function can be asynchronous, in which case it should return
36 | * a Promise.
37 | *
38 | * @example
39 | *
40 | * post.editionView().onSubmitSuccess(['progression', 'notification', '$state', 'entry', 'entity', function(progression, notification, $state, entry, entity) {
41 | * // stop the progress bar
42 | * progression.done();
43 | * // add a notification
44 | * notification.log(`Element #${entry._identifierValue} successfully edited.`, { addnCls: 'humane-flatty-success' });
45 | * // redirect to the list view
46 | * $state.go($state.get('list'), { entity: entity.name() });
47 | * // cancel the default action (redirect to the edition view)
48 | * return false;
49 | * }])
50 | */
51 | onSubmitSuccess(onSubmitSuccess) {
52 | if (!arguments.length) return this._onSubmitSuccess;
53 | this._onSubmitSuccess = onSubmitSuccess;
54 | return this;
55 | }
56 |
57 | /**
58 | * Add a function to be executed after the update request receives a failed
59 | * http response from the server.
60 | *
61 | * This is the ideal place to use the response to update the entry, display
62 | * server-side validation error, or redirect to another view.
63 | *
64 | * If the function returns false, the default execution workflow is stopped.
65 | * This means that the function must provide a custom workflow.
66 | *
67 | * The syntax depends on the framework calling the function.
68 | *
69 | * With ng-admin, the function can be an angular injectable, listing
70 | * required dependencies in an array. Among other, the function can receive
71 | * the following services:
72 | * - $event: the form submission event
73 | * - error: the response from the server
74 | * - errorMessage: the error message based on the response
75 | * - entry: the current Entry instance
76 | * - entity: the current entity
77 | * - form: the form object (for form validation and errors)
78 | * - progression: the controller for the loading indicator
79 | * - notification: the controller for top notifications
80 | *
81 | * The function can be asynchronous, in which case it should return
82 | * a Promise.
83 | *
84 | * @example
85 | *
86 | * post.editionView().onSubmitError(['error', 'form', 'progression', 'notification', function(error, form, progression, notification) {
87 | * // mark fields based on errors from the response
88 | * error.violations.forEach(violation => {
89 | * if (form[violation.propertyPath]) {
90 | * form[violation.propertyPath].$valid = false;
91 | * }
92 | * });
93 | * // stop the progress bar
94 | * progression.done();
95 | * // add a notification
96 | * notification.log(`Some values are invalid, see details in the form`, { addnCls: 'humane-flatty-error' });
97 | * // cancel the default action (default error messages)
98 | * return false;
99 | * }]);
100 | */
101 | onSubmitError(onSubmitError) {
102 | if (!arguments.length) return this._onSubmitError;
103 | this._onSubmitError = onSubmitError;
104 | return this;
105 | }
106 | }
107 |
108 | export default EditView;
109 |
--------------------------------------------------------------------------------
/src/View/CreateView.js:
--------------------------------------------------------------------------------
1 | import View from './View';
2 |
3 | class CreateView extends View {
4 | constructor(name) {
5 | super(name);
6 | this._type = 'CreateView';
7 | this._submitCreationSuccess = null;
8 | this._submitCreationError = null;
9 | }
10 |
11 | /**
12 | * Add a function to be executed after the creation succeeds.
13 | *
14 | * This is the ideal place to use the response to update the entry, or
15 | * redirect to another view.
16 | *
17 | * If the function returns false, the default execution workflow is stopped.
18 | * This means that the function must provide a custom workflow.
19 | *
20 | * If the function throws an exception, the onSubmitError callback will
21 | * execute.
22 | *
23 | * The syntax depends on the framework calling the function.
24 | *
25 | * With ng-admin, the function can be an angular injectable, listing
26 | * required dependencies in an array. Among other, the function can receive
27 | * the following services:
28 | * - $event: the form submission event
29 | * - entry: the current Entry instance
30 | * - entity: the current entity
31 | * - form: the form object (for form validation and errors)
32 | * - progression: the controller for the loading indicator
33 | * - notification: the controller for top notifications
34 | *
35 | * The function can be asynchronous, in which case it should return
36 | * a Promise.
37 | *
38 | * @example
39 | *
40 | * post.creationView().onSubmitSuccess(['progression', 'notification', '$state', 'entry', 'entity', function(progression, notification, $state, entry, entity) {
41 | * // stop the progress bar
42 | * progression.done();
43 | * // add a notification
44 | * notification.log(`Element #${entry._identifierValue} successfully created.`, { addnCls: 'humane-flatty-success' });
45 | * // redirect to the list view
46 | * $state.go($state.get('list'), { entity: entity.name() });
47 | * // cancel the default action (redirect to the edition view)
48 | * return false;
49 | * }])
50 | *
51 | */
52 | onSubmitSuccess(onSubmitSuccess) {
53 | if (!arguments.length) return this._onSubmitSuccess;
54 | this._onSubmitSuccess = onSubmitSuccess;
55 | return this;
56 | }
57 |
58 | /**
59 | * Add a function to be executed after the creation request receives a
60 | * failed http response from the server.
61 | *
62 | * This is the ideal place to use the response to update the entry, display
63 | * server-side validation error, or redirect to another view.
64 | *
65 | * If the function returns false, the default execution workflow is stopped.
66 | * This means that the function must provide a custom workflow.
67 | *
68 | * The syntax depends on the framework calling the function.
69 | *
70 | * With ng-admin, the function can be an angular injectable, listing
71 | * required dependencies in an array. Among other, the function can receive
72 | * the following services:
73 | * - $event: the form submission event
74 | * - error: the response from the server
75 | * - errorMessage: the error message based on the response
76 | * - entry: the current Entry instance
77 | * - entity: the current entity
78 | * - form: the form object (for form validation and errors)
79 | * - progression: the controller for the loading indicator
80 | * - notification: the controller for top notifications
81 | *
82 | * The function can be asynchronous, in which case it should return
83 | * a Promise.
84 | *
85 | * @example
86 | *
87 | * post.creationView().onSubmitError(['error', 'form', 'progression', 'notification', function(error, form, progression, notification) {
88 | * // mark fields based on errors from the response
89 | * error.violations.forEach(violation => {
90 | * if (form[violation.propertyPath]) {
91 | * form[violation.propertyPath].$valid = false;
92 | * }
93 | * });
94 | * // stop the progress bar
95 | * progression.done();
96 | * // add a notification
97 | * notification.log(`Some values are invalid, see details in the form`, { addnCls: 'humane-flatty-error' });
98 | * // cancel the default action (default error messages)
99 | * return false;
100 | * }]);
101 | */
102 | onSubmitError(onSubmitError) {
103 | if (!arguments.length) return this._onSubmitError;
104 | this._onSubmitError = onSubmitError;
105 | return this;
106 | }
107 | }
108 |
109 | export default CreateView;
110 |
--------------------------------------------------------------------------------
/tests/lib/Entity/EntityTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Entity from '../../../lib/Entity/Entity';
4 | import Field from '../../../lib/Field/Field';
5 |
6 | describe('Entity', function() {
7 | describe('views', function() {
8 | it('should create all views when creating new entity', function() {
9 | var entity = new Entity('post');
10 | assert.deepEqual([
11 | 'DashboardView',
12 | 'MenuView',
13 | 'ListView',
14 | 'CreateView',
15 | 'EditView',
16 | 'DeleteView',
17 | 'BatchDeleteView',
18 | 'ExportView',
19 | 'ShowView'
20 | ], Object.keys(entity.views));
21 | });
22 | });
23 |
24 | describe('label', function() {
25 | it('should return given label if already set', function() {
26 | var post = new Entity('post').label('Article');
27 | assert.equal('Article', post.label());
28 | });
29 |
30 | it('should return entity name if no label has been set', function() {
31 | var post = new Entity('post');
32 | assert.equal('Post', post.label());
33 | });
34 | });
35 |
36 | describe('readOnly()', function() {
37 | var entity;
38 |
39 | beforeEach(function() {
40 | entity = new Entity('post');
41 | });
42 |
43 | it('should not be read-only by default', function() {
44 | assert.equal(false, entity.isReadOnly);
45 | });
46 |
47 | it('should set read-only attribute', function() {
48 | entity.readOnly();
49 | assert.equal(true, entity.isReadOnly);
50 | });
51 |
52 | it('should disable all edition views', function() {
53 | entity.readOnly();
54 | entity.views.ListView.enable();
55 | entity.views.DashboardView.enable();
56 |
57 | assert.equal(true, entity.menuView().enabled);
58 | assert.equal(true, entity.dashboardView().enabled);
59 | assert.equal(true, entity.listView().enabled);
60 | assert.equal(false, entity.creationView().enabled);
61 | assert.equal(false, entity.editionView().enabled);
62 | assert.equal(false, entity.deletionView().enabled);
63 | });
64 | });
65 |
66 | describe('createMethod', function() {
67 | it('should return given createMethod if already set', function() {
68 | var post = new Entity('post').createMethod('put');
69 | assert.equal('put', post.createMethod());
70 | });
71 |
72 | it('should return null if no createMethod has been set', function() {
73 | var post = new Entity('post');
74 | assert.equal(null, post.createMethod());
75 | });
76 | });
77 |
78 | describe('updateMethod', function() {
79 | it('should return given updateMethod if already set', function() {
80 | var post = new Entity('post').updateMethod('post');
81 | assert.equal('post', post.updateMethod());
82 | });
83 |
84 | it('should return null if no updateMethod has been set', function() {
85 | var post = new Entity('post');
86 | assert.equal(null, post.updateMethod());
87 | });
88 | });
89 |
90 | describe('retrieveMethod', function() {
91 | it('should return given retrieveMethod if already set', function() {
92 | var post = new Entity('post').retrieveMethod('post');
93 | assert.equal('post', post.retrieveMethod());
94 | });
95 |
96 | it('should return null if no retrieveMethod has been set', function() {
97 | var post = new Entity('post');
98 | assert.equal(null, post.retrieveMethod());
99 | });
100 | });
101 |
102 | describe('deleteMethod', function() {
103 | it('should return given deleteMethod if already set', function() {
104 | var post = new Entity('post').deleteMethod('post');
105 | assert.equal('post', post.deleteMethod());
106 | });
107 |
108 | it('should return null if no deleteMethod has been set', function() {
109 | var post = new Entity('post');
110 | assert.equal(null, post.deleteMethod());
111 | });
112 | });
113 |
114 | describe('identifier', function() {
115 | it('should set default identifier', function() {
116 | var post = new Entity('post');
117 | assert.equal('id', post.identifier().name());
118 | });
119 |
120 | it('should set custom identifier', function() {
121 | var post = new Entity('post').identifier(new Field('my_id'));
122 | assert.equal('my_id', post.identifier().name());
123 | });
124 |
125 | it('should throw error on wrong argument', function() {
126 | assert.throw(function () { new Entity('post').identifier('my_id'); }, Error, 'Entity post: identifier must be an instance of Field');
127 | });
128 | });
129 | });
130 |
--------------------------------------------------------------------------------
/tests/lib/DataStore/DataStore.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import DataStore from "../../../lib/DataStore/DataStore";
4 | import Entity from "../../../lib/Entity/Entity";
5 | import Entry from "../../../lib/Entry";
6 | import Field from "../../../lib/Field/Field";
7 | import ReferenceField from "../../../lib/Field/ReferenceField";
8 | import ReferenceManyField from "../../../lib/Field/ReferenceManyField";
9 | import View from "../../../lib/View/View";
10 |
11 | describe('DataStore', function() {
12 | var dataStore;
13 |
14 | beforeEach(function() {
15 | dataStore = new DataStore();
16 | });
17 |
18 | describe('getReferenceChoicesById', function () {
19 | it('should retrieve choices by id.', function () {
20 | var ref = new ReferenceField('human_id'),
21 | human = new Entity('human');
22 |
23 | human
24 | .identifier(new Field('id'))
25 | .editionView();
26 |
27 | ref
28 | .targetEntity(human)
29 | .targetField(new Field('name'));
30 |
31 | dataStore.setEntries(human.uniqueId + '_values', [
32 | {values: { id: 1, human_id: 1, name: 'Suna'}},
33 | {values: { id: 2, human_id: 2, name: 'Boby'}},
34 | {values: { id: 3, human_id: 1, name: 'Mizute'}}
35 | ]);
36 |
37 | var choices = dataStore.getReferenceChoicesById(ref);
38 | assert.equal(ref.type(), 'reference');
39 | assert.deepEqual(choices, {
40 | 1: 'Suna',
41 | 2: 'Boby',
42 | 3: 'Mizute'
43 | });
44 | });
45 | });
46 |
47 | describe('getChoices', function () {
48 | it('should retrieve choices.', function () {
49 | var ref = new ReferenceField('human_id'),
50 | human = new Entity('human');
51 |
52 | human
53 | .identifier(new Field('id'))
54 | .editionView();
55 |
56 | ref
57 | .targetField(new Field('name'))
58 | .targetEntity(human);
59 |
60 | dataStore.setEntries(human.uniqueId + '_choices', [
61 | new Entry('human', { id: 1, human_id: 1, name: 'Suna'}),
62 | new Entry('human', { id: 2, human_id: 2, name: 'Boby'}),
63 | new Entry('human', { id: 3, human_id: 1, name: 'Mizute'})
64 | ]);
65 |
66 | assert.equal(ref.type(), 'reference');
67 | assert.deepEqual(dataStore.getChoices(ref), [
68 | { value: 1, label: 'Suna'},
69 | { value: 2, label: 'Boby'},
70 | { value: 3, label: 'Mizute'}
71 | ]);
72 | });
73 | });
74 |
75 | describe('fillReferencesValuesFromCollection', function() {
76 | it('should fill reference values of a collection', function () {
77 | var entry1 = new Entry(),
78 | entry2 = new Entry(),
79 | entry3 = new Entry(),
80 | human = new Entity('humans'),
81 | tag = new Entity('tags'),
82 | ref1 = new ReferenceField('human_id'),
83 | ref2 = new ReferenceManyField('tags');
84 |
85 | human.editionView().identifier(new Field('id'));
86 | tag.editionView().identifier(new Field('id'));
87 | ref1
88 | .targetEntity(human)
89 | .targetField(new Field('name'));
90 | dataStore.setEntries(ref1.targetEntity().uniqueId + '_values', [
91 | {values: {id: 1, name: 'Bob'}},
92 | {values: {id: 2, name: 'Daniel'}},
93 | {values: {id: 3, name: 'Jack'}}
94 | ]);
95 |
96 | ref2
97 | .targetEntity(tag)
98 | .targetField(new Field('label'));
99 | dataStore.setEntries(ref2.targetEntity().uniqueId + '_values', [
100 | {values: {id: 1, label: 'Photo'}},
101 | {values: {id: 2, label: 'Watch'}},
102 | {values: {id: 3, label: 'Panda'}}
103 | ]);
104 |
105 | entry1.values.human_id = 1;
106 | entry1.values.tags = [1, 3];
107 | entry2.values.human_id = 1;
108 | entry2.values.tags = [2];
109 | entry3.values.human_id = 3;
110 |
111 | var collection = [entry1, entry2, entry3];
112 | var referencedValues = {
113 | human_id: ref1,
114 | tags: ref2
115 | };
116 |
117 | collection = dataStore.fillReferencesValuesFromCollection(collection, referencedValues, true);
118 |
119 | assert.equal(collection.length, 3);
120 | assert.equal(collection[0].listValues.human_id, 'Bob');
121 | assert.deepEqual(collection[0].listValues.tags, ['Photo', 'Panda']);
122 | assert.deepEqual(collection[1].listValues.tags, ['Watch']);
123 | assert.equal(collection[2].listValues.human_id, 'Jack');
124 | assert.deepEqual(collection[2].listValues.tags, []);
125 | });
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/tests/lib/EntryTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Entity from "../../lib/Entity/Entity";
4 | import Entry from "../../lib/Entry";
5 | import Field from "../../lib/Field/Field";
6 | import ReferenceManyField from "../../lib/Field/ReferenceManyField";
7 | import JsonField from "../../lib/Field/JsonField";
8 |
9 | describe('Entry', function() {
10 |
11 | describe('createFromRest()', function() {
12 |
13 | it('should return an entry with no value if REST entry is empty and fields is empty', function() {
14 | var mappedEntry = Entry.createFromRest({}, []);
15 | assert.deepEqual({}, mappedEntry.values);
16 | });
17 |
18 | it('should return an entry with default values if REST entry is empty', function() {
19 | var fields = [
20 | new Field('id'),
21 | new Field('title').defaultValue('The best title'),
22 | new Field('author.name'),
23 | new ReferenceManyField('tags'),
24 | new JsonField('address').defaultValue({ number: null, street: null, city: null })
25 | ];
26 | var mappedEntry = Entry.createFromRest({}, fields);
27 | assert.deepEqual({
28 | id: null,
29 | title: 'The best title',
30 | 'author.name': null,
31 | tags: null,
32 | address: { number: null, street: null, city: null }
33 | }, mappedEntry.values);
34 | });
35 |
36 | it('should map each value to related field if existing', () => {
37 | var mappedEntry = Entry.createFromRest({
38 | id: 1,
39 | title: 'ng-admin + ES6 = pure awesomeness!',
40 | body: 'Really, it rocks!',
41 | tags: [1, 2, 4]
42 | });
43 |
44 | assert.deepEqual({
45 | id: 1,
46 | title: 'ng-admin + ES6 = pure awesomeness!',
47 | body: 'Really, it rocks!',
48 | tags: [1, 2, 4]
49 | }, mappedEntry.values);
50 | });
51 |
52 | it('should map even if the value is absent', () => {
53 | var fields = [new Field('foo').map((v,e) => e.tags.join(' '))];
54 | var mappedEntry = Entry.createFromRest({
55 | tags: [1, 2, 3, 4]
56 | }, fields);
57 | assert.deepEqual({
58 | foo: '1 2 3 4',
59 | tags: [1, 2, 3, 4]
60 | }, mappedEntry.values)
61 | });
62 |
63 | it('should set as identifierValue value for identifier field', () => {
64 | var mappedEntry = Entry.createFromRest({ id: 1 });
65 | assert.equal(1, mappedEntry.identifierValue);
66 | });
67 |
68 | it('should set as identifierValue value using identifier field name', () => {
69 | var mappedEntry = Entry.createFromRest({ id: 1, foo: 2 }, [], '', 'foo');
70 | assert.equal(2, mappedEntry.identifierValue);
71 | });
72 |
73 | it('should flatten nested entries', () => {
74 | var mappedEntry = Entry.createFromRest({
75 | id: 1,
76 | title: 'ng-admin + ES6 = pure awesomeness!',
77 | author: { name: 'John Doe' }
78 | });
79 |
80 | assert.deepEqual({
81 | id: 1,
82 | title: 'ng-admin + ES6 = pure awesomeness!',
83 | 'author.name': 'John Doe'
84 | }, mappedEntry.values);
85 | });
86 |
87 | it('should not flatten nested entries mapped by a not flattenable field', () => {
88 | var mappedEntry = Entry.createFromRest({
89 | id: 1,
90 | title: 'ng-admin + ES6 = pure awesomeness!',
91 | address: { number: 21, street: 'JumpStreet', city: 'Vancouver' } // mapped as JSON field => not flattenable
92 | }, [new JsonField('address')]);
93 |
94 | assert.deepEqual({
95 | id: 1,
96 | title: 'ng-admin + ES6 = pure awesomeness!',
97 | address: { number: 21, street: 'JumpStreet', city: 'Vancouver' }
98 | }, mappedEntry.values);
99 | });
100 |
101 | it('should apply map() functions from fields', () => {
102 | var mappedEntry = Entry.createFromRest({
103 | id: 1,
104 | title: 'ng-admin + ES6 = pure awesomeness!'
105 | }, [new Field('title').map(s => s.toUpperCase())]);
106 | assert.deepEqual({
107 | id: 1,
108 | title: 'NG-ADMIN + ES6 = PURE AWESOMENESS!'
109 | }, mappedEntry.values);
110 | })
111 | });
112 |
113 | describe('transformToRest()', function() {
114 | it('should provide both the value and entry when invoking the callback', () => {
115 | var field = new Field('foo');
116 | field.defaultValue(2);
117 |
118 | var entry = Entry.createForFields([field]);
119 |
120 | field.transform((value, entry) => {
121 | assert.equal(value, 2);
122 | assert.deepEqual(entry, {foo: 2});
123 | });
124 |
125 | entry.transformToRest([field]);
126 | });
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/tests/lib/View/ListViewTest.js:
--------------------------------------------------------------------------------
1 | /* global describe,it */
2 |
3 | var assert = require('chai').assert;
4 |
5 | import Entity from '../../../lib/Entity/Entity';
6 | import Entry from '../../../lib/Entry';
7 | import Field from '../../../lib/Field/Field';
8 | import ReferenceField from '../../../lib/Field/ReferenceField';
9 | import ReferenceManyField from '../../../lib/Field/ReferenceManyField';
10 | import ListView from '../../../lib/View/ListView';
11 |
12 | describe('ListView', function() {
13 | describe('listActions()', function () {
14 | it('should return the view', function () {
15 | var view = new ListView();
16 | assert.equal(view.listActions(['edit']), view);
17 | });
18 |
19 | it('should store the listActions for the Datagrid', function () {
20 | var view = new ListView();
21 | assert.deepEqual(view.listActions(['edit']).listActions(), ['edit']);
22 | });
23 | });
24 |
25 | describe('map()', function () {
26 | it('should apply the function argument to all list values', function () {
27 | var list = new ListView('allCats');
28 | list
29 | .setEntity(new Entity('cats').identifier(new Field('id')))
30 | .addField(new Field('name').map(function (value) {
31 | return value.substr(0, 5) + '...';
32 | }));
33 |
34 | var entries = list.mapEntries([
35 | { id: 1, human_id: 1, name: 'Suna'},
36 | { id: 2, human_id: 2, name: 'Boby'},
37 | { id: 3, human_id: 1, name: 'Mizute'}
38 | ]);
39 |
40 | assert.equal(entries[0].values.id, 1);
41 | assert.equal(entries[0].values.name, 'Suna...');
42 | assert.equal(entries[2].values.id, 3);
43 | assert.equal(entries[2].values.name, 'Mizut...');
44 | });
45 | });
46 |
47 | describe('getFilterReferences()', function() {
48 | it('should return only reference and reference_many fields', function() {
49 | var post = new Entity('post');
50 | var category = new ReferenceField('category');
51 | var tags = new ReferenceManyField('tags');
52 | var view = new ListView(post)
53 | .fields([
54 | new Field('title'),
55 | tags
56 | ])
57 | .filters([
58 | category
59 | ]);
60 |
61 | assert.deepEqual({category: category}, view.getFilterReferences());
62 | });
63 |
64 | it('should return only filter reference with refresh complete if withRemoteComplete is true', function() {
65 | var post = new Entity('post');
66 | var category = new ReferenceField('category').remoteComplete(true);
67 | var tags = new ReferenceManyField('tags').remoteComplete(false);
68 |
69 | var view = new ListView(post)
70 | .fields([
71 | new Field('title'),
72 | tags
73 | ])
74 | .filters([
75 | category
76 | ]);
77 |
78 | assert.deepEqual({category: category}, view.getFilterReferences(true));
79 | });
80 |
81 | it('should return only filter reference with no remote complete if withRemoteComplete is set to false', function() {
82 | var post = new Entity('post');
83 | var category = new ReferenceField('category').remoteComplete(true);
84 | var tags = new ReferenceManyField('tags').remoteComplete(false);
85 | var view = new ListView(post)
86 | .fields([
87 | new Field('title'),
88 | tags
89 | ])
90 | .filters([
91 | category
92 | ]);
93 |
94 | assert.deepEqual({ tags: tags }, view.getReferences(false));
95 | });
96 | });
97 |
98 | describe('getEntryCssClasses', function() {
99 | var view;
100 |
101 | beforeEach(function() {
102 | view = new ListView(new Entity('post'));
103 | });
104 |
105 | it('should return result of callback called with entry if function', function() {
106 | view.entryCssClasses(entry => entry.values.id % 2 ? "odd-entry" : "even-entry");
107 |
108 | var entry = new Entry('post', { id: 1 });
109 | assert.equal("odd-entry", view.getEntryCssClasses(entry));
110 |
111 | entry = new Entry('post', { id: 2 });
112 | assert.equal("even-entry", view.getEntryCssClasses(entry));
113 | });
114 |
115 | it('should concatenate all elements if array', function() {
116 | view.entryCssClasses(["important", "approved"]);
117 | assert.equal("important approved", view.getEntryCssClasses());
118 | });
119 |
120 | it('should return passed classes if neither function nor array', function() {
121 | view.entryCssClasses("important approved");
122 | assert.equal("important approved", view.getEntryCssClasses());
123 | });
124 |
125 | it('should return an empty string by default', function() {
126 | assert.equal(view.getEntryCssClasses(), '');
127 | });
128 | });
129 |
130 | });
131 |
--------------------------------------------------------------------------------
/src/Field/ReferenceField.js:
--------------------------------------------------------------------------------
1 | import Field from "./Field";
2 |
3 | class ReferenceField extends Field {
4 | constructor(name) {
5 | super(name);
6 | this._type = 'reference';
7 | this._targetEntity = null;
8 | this._targetField = null;
9 | this._perPage = 30;
10 | this._permanentFilters = null;
11 | this._sortField = null;
12 | this._sortDir = null;
13 | this._singleApiCall = false;
14 | this._detailLink = true;
15 | this._remoteComplete = false;
16 | this._remoteCompleteOptions= {
17 | refreshDelay: 500
18 | };
19 | }
20 |
21 | perPage(perPage) {
22 | if (!arguments.length) return this._perPage;
23 | this._perPage = perPage;
24 | return this;
25 | }
26 |
27 | datagridName() {
28 | return this._targetEntity.name() + '_ListView';
29 | }
30 |
31 | targetEntity(entity) {
32 | if (!arguments.length) {
33 | return this._targetEntity;
34 | }
35 | this._targetEntity = entity;
36 |
37 | return this;
38 | }
39 |
40 | targetField(field) {
41 | if (!arguments.length) return this._targetField;
42 | this._targetField = field;
43 |
44 | return this;
45 | }
46 |
47 | /**
48 | * Define permanent filters to be added to the REST API calls
49 | *
50 | * nga.field('post_id', 'reference').permanentFilters({
51 | * published: true
52 | * });
53 | * // related API call will be /posts/:id?published=true
54 | *
55 | * @param {Object} filters list of filters to apply to the call
56 | */
57 | permanentFilters(filters) {
58 | if (!arguments.length) {
59 | return this._permanentFilters;
60 | }
61 |
62 | this._permanentFilters = filters;
63 |
64 | return this;
65 | }
66 |
67 | /**
68 | * @deprecated use permanentFilters() instead
69 | */
70 | filters(filters) {
71 | console.warn('ReferenceField.filters() is deprecated, please use ReferenceField.permanentFilters() instead');
72 | return this.permanentFilters(filters);
73 | }
74 |
75 | sortField() {
76 | if (arguments.length) {
77 | this._sortField = arguments[0];
78 | return this;
79 | }
80 |
81 | return this._sortField;
82 | }
83 |
84 | sortDir() {
85 | if (arguments.length) {
86 | this._sortDir = arguments[0];
87 | return this;
88 | }
89 |
90 | return this._sortDir;
91 | }
92 |
93 | singleApiCall(singleApiCall) {
94 | if (!arguments.length) return this._singleApiCall;
95 | this._singleApiCall = singleApiCall;
96 | return this;
97 | }
98 |
99 | hasSingleApiCall() {
100 | return typeof this._singleApiCall === 'function';
101 | }
102 |
103 | getSingleApiCall(identifiers) {
104 | return this.hasSingleApiCall() ? this._singleApiCall(identifiers) : this._singleApiCall;
105 | }
106 |
107 | getIdentifierValues(rawValues) {
108 | let results = {};
109 | let identifierName = this._name;
110 | for (let i = 0, l = rawValues.length ; i < l ; i++) {
111 | let identifier = rawValues[i][identifierName];
112 | if (identifier == null) {
113 | continue;
114 | }
115 |
116 | if (identifier instanceof Array) {
117 | for (let j in identifier) {
118 | results[identifier[j]] = true;
119 | }
120 | continue;
121 | }
122 |
123 | results[identifier] = true;
124 | }
125 |
126 | return Object.keys(results);
127 | }
128 |
129 | getSortFieldName() {
130 | if (!this.sortField()) {
131 | return null;
132 | }
133 |
134 | return this._targetEntity.name() + '_ListView.' + this.sortField();
135 | }
136 |
137 | /**
138 | * Enable autocompletion using REST API for choices.
139 | *
140 | * Available options are:
141 | *
142 | * * `refreshDelay`: minimal delay between two API calls in milliseconds. By default: 500.
143 | * * `searchQuery`: a function returning the parameters to add to the query string basd on the input string.
144 | *
145 | * new ReferenceField('authors')
146 | * .targetEntity(author)
147 | * .targetField(new Field('name'))
148 | * .remoteComplete(true, {
149 | * refreshDelay: 300,
150 | * // populate choices from the response of GET /tags?q=XXX
151 | * searchQuery: function(search) { return { q: search }; }
152 | * })
153 | * .perPage(10) // limit the number of results to 10
154 | *
155 | * @param {Boolean} remoteComplete true to enable remote complete. False by default
156 | * @param {Object} options Remote completion options (optional)
157 | */
158 | remoteComplete(remoteComplete, options) {
159 | if (!arguments.length) return this._remoteComplete;
160 | this._remoteComplete = remoteComplete;
161 | if (options) {
162 | this.remoteCompleteOptions(options);
163 | }
164 | return this;
165 | }
166 |
167 | remoteCompleteOptions(options) {
168 | if (!arguments.length) return this._remoteCompleteOptions;
169 | this._remoteCompleteOptions = options;
170 | return this;
171 | }
172 | }
173 |
174 | export default ReferenceField;
175 |
--------------------------------------------------------------------------------
/src/View/ListView.js:
--------------------------------------------------------------------------------
1 | import View from './View';
2 | import orderElement from "../Utils/orderElement";
3 |
4 | class ListView extends View {
5 | constructor(name) {
6 | super(name);
7 |
8 | this._type = 'ListView';
9 | this._perPage = 30;
10 | this._infinitePagination = false;
11 | this._listActions = [];
12 | this._batchActions = ['delete'];
13 | this._filters = [];
14 | this._permanentFilters = {};
15 | this._exportFields = null;
16 | this._exportOptions = {};
17 | this._entryCssClasses = null;
18 |
19 | this._sortField = 'id';
20 | this._sortDir = 'DESC';
21 | }
22 |
23 | perPage() {
24 | if (!arguments.length) { return this._perPage; }
25 | this._perPage = arguments[0];
26 | return this;
27 | }
28 |
29 | /** @deprecated Use perPage instead */
30 | limit() {
31 | if (!arguments.length) { return this.perPage(); }
32 | return this.perPage(arguments[0]);
33 | }
34 |
35 | sortField() {
36 | if (arguments.length) {
37 | this._sortField = arguments[0];
38 | return this;
39 | }
40 |
41 | return this._sortField;
42 | }
43 |
44 | sortDir() {
45 | if (arguments.length) {
46 | this._sortDir = arguments[0];
47 | return this;
48 | }
49 |
50 | return this._sortDir;
51 | }
52 |
53 | getSortFieldName() {
54 | return this.name() + '.' + this._sortField;
55 | }
56 |
57 | infinitePagination() {
58 | if (arguments.length) {
59 | this._infinitePagination = arguments[0];
60 | return this;
61 | }
62 |
63 | return this._infinitePagination;
64 | }
65 |
66 | actions(actions) {
67 | if (!arguments.length) {
68 | return this._actions;
69 | }
70 |
71 | this._actions = actions;
72 |
73 | return this;
74 | }
75 |
76 | exportFields(exportFields) {
77 | if (!arguments.length) {
78 | return this._exportFields;
79 | }
80 |
81 | this._exportFields = exportFields;
82 |
83 | return this;
84 | }
85 |
86 | exportOptions(exportOptions) {
87 | if (!arguments.length) {
88 | return this._exportOptions;
89 | }
90 |
91 | this._exportOptions = exportOptions;
92 |
93 | return this;
94 | }
95 |
96 | batchActions(actions) {
97 | if (!arguments.length) {
98 | return this._batchActions;
99 | }
100 |
101 | this._batchActions = actions;
102 |
103 | return this;
104 | }
105 |
106 | /**
107 | * Define permanent filters to be added to the REST API calls
108 | *
109 | * posts.listView().permanentFilters({
110 | * published: true
111 | * });
112 | * // related API call will be /posts?published=true
113 | *
114 | * @param {Object} filters list of filters to apply to the call
115 | */
116 | permanentFilters(filters) {
117 | if (!arguments.length) {
118 | return this._permanentFilters;
119 | }
120 |
121 | this._permanentFilters = filters;
122 |
123 | return this;
124 | }
125 |
126 | /**
127 | * Define filters the user can add to the datagrid
128 | *
129 | * posts.listView().filters([
130 | * nga.field('title'),
131 | * nga.field('age', 'number')
132 | * ]);
133 | *
134 | * @param {Field[]} filters list of filters to add to the GUI
135 | */
136 | filters(filters) {
137 | if (!arguments.length) {
138 | return this._filters;
139 | }
140 |
141 | this._filters = orderElement.order(filters);
142 |
143 | return this;
144 | }
145 |
146 | getFilterReferences(withRemoteComplete) {
147 | let result = {};
148 | let lists = this._filters.filter(f => f.type() === 'reference');
149 |
150 | var filterFunction = null;
151 | if (withRemoteComplete === true) {
152 | filterFunction = f => f.remoteComplete();
153 | } else if (withRemoteComplete === false) {
154 | filterFunction = f => !f.remoteComplete();
155 | }
156 |
157 | if (filterFunction !== null) {
158 | lists = lists.filter(filterFunction);
159 | }
160 |
161 | for (let i = 0, c = lists.length ; i < c ; i++) {
162 | let list = lists[i];
163 | result[list.name()] = list;
164 | }
165 |
166 | return result;
167 | }
168 |
169 | listActions(actions) {
170 | if (!arguments.length) {
171 | return this._listActions;
172 | }
173 |
174 | this._listActions = actions;
175 |
176 | return this;
177 | }
178 |
179 | entryCssClasses(classes) {
180 | if (!arguments.length) {
181 | return this._entryCssClasses;
182 | }
183 |
184 | this._entryCssClasses = classes;
185 |
186 | return this;
187 | }
188 |
189 | getEntryCssClasses(entry) {
190 | if (!this._entryCssClasses) {
191 | return '';
192 | }
193 |
194 | if (this._entryCssClasses.constructor === Array) {
195 | return this._entryCssClasses.join(' ');
196 | }
197 |
198 | if (typeof(this._entryCssClasses) === 'function') {
199 | return this._entryCssClasses(entry);
200 | }
201 |
202 | return this._entryCssClasses;
203 | }
204 | }
205 |
206 | export default ListView;
207 |
--------------------------------------------------------------------------------
/tests/lib/Menu/MenuTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Menu from "../../../lib/Menu/Menu";
4 | import ChoicesField from "../../../lib/Field/ChoicesField";
5 | import Entity from "../../../lib/Entity/Entity";
6 |
7 | describe('Menu', () => {
8 |
9 | describe('constructor', () => {
10 | it('should return an empty menu', () => {
11 | var menu = new Menu();
12 | assert.isNull(menu.link());
13 | assert.isNull(menu.title());
14 | assert.isFalse(menu.icon());
15 | assert.deepEqual([], menu.children());
16 | assert.isFalse(menu.template());
17 | })
18 | });
19 |
20 | describe('link', () => {
21 | it('should set link and active function', () => {
22 | var menu = new Menu().link('/foo/bar');
23 | assert.equal('/foo/bar', menu.link());
24 | assert.isTrue(menu.isActive('/foo/bar'));
25 | });
26 | it('should return link when called with no argument', () =>
27 | assert.equal('/foo/bar', new Menu().link('/foo/bar').link())
28 | );
29 | });
30 |
31 | describe('isActive', () => {
32 | it('should return false by default', () =>
33 | assert.isFalse(new Menu().isActive())
34 | );
35 | it('should return true for the link', () =>
36 | assert.isTrue(new Menu().link('/foo').isActive('/foo'))
37 | );
38 | it('should return true for the link followed by something else', () =>
39 | assert.isTrue(new Menu().link('/foo').isActive('/foo/bar'))
40 | );
41 | it('should return false for something different than link', () =>
42 | assert.isFalse(new Menu().link('/foo/bar').isActive('/foo/far'))
43 | );
44 | });
45 |
46 | describe('active', () => {
47 | it('should override the function used to determine if the menu is active', () =>
48 | assert.isTrue(new Menu().link('/foo/bar').active(() => true).isActive('/foo/far'))
49 | );
50 | });
51 |
52 | describe('addChild', () => {
53 | it('should not accept anything other than a Menu', () =>
54 | assert.throw(() => new Menu().addChild('foo'))
55 | );
56 | it('should add a Child Menu', () => {
57 | var menu = new Menu();
58 | assert.isFalse(menu.hasChild());
59 | var submenu = new Menu();
60 | menu.addChild(submenu);
61 | assert.isTrue(menu.hasChild());
62 | assert.deepEqual([submenu], menu.children());
63 | });
64 | });
65 |
66 | describe('getChildByTitle', () => {
67 | it('should return undefined when no child', () => {
68 | assert.isUndefined(new Menu().getChildByTitle('foo'));
69 | });
70 | it('should return the first matching Menu', () => {
71 | let menu = new Menu();
72 | let fooMenu = new Menu().title('Foo');
73 | menu
74 | .addChild(fooMenu)
75 | .addChild(new Menu().title('Bar'));
76 | assert.deepEqual(fooMenu, menu.getChildByTitle('Foo'));
77 | })
78 | });
79 |
80 | describe('icon', () => {
81 | it('should return false by default', () => assert.isFalse(new Menu().icon()));
82 | it('should set the icon', () => assert.equal('foo', new Menu().icon('foo').icon()))
83 | });
84 |
85 | describe('template', () => {
86 | it('should return false by default', () => assert.isFalse(new Menu().template()));
87 | it('should set the template', () => assert.equal('foo', new Menu().template('foo').template()))
88 | });
89 |
90 | describe('populateFromEntity', () => {
91 | it('should fail if passed anything else than an Entity', () => {
92 | assert.throw(() => new Menu().populateFromEntity('foo'), 'populateFromEntity() only accepts an Entity parameter')
93 | });
94 | it('should set label according to Entity', () => {
95 | assert.equal('Comments', new Menu().populateFromEntity(new Entity('comments')).title());
96 | });
97 | it('should set link according to entity', () => {
98 | assert.equal('/comments/list', new Menu().populateFromEntity(new Entity('comments')).link());
99 | });
100 | it('should set active function to entity', () => {
101 | let menu = new Menu().populateFromEntity(new Entity('comments'));
102 | assert.isTrue(menu.isActive('/comments/list'));
103 | assert.isTrue(menu.isActive('/comments/edit/2'));
104 | assert.isFalse(menu.isActive('/posts/list'));
105 | });
106 | it('should set icon according to MenuView', () => {
107 | let entity = new Entity('comments');
108 | entity.menuView().icon('');
109 | assert.equal('', new Menu().populateFromEntity(entity).icon());
110 | });
111 |
112 | it('should include pinned filters with default value inside link', () => {
113 | let entity = new Entity('comments');
114 |
115 | const statusFilter = new ChoicesField('status');
116 | statusFilter.choices([
117 | { label: 'rejected', value: 'rejected' },
118 | { label: 'approved', value: 'approved' },
119 | { label: 'pending', value: 'pending' },
120 | ]);
121 | statusFilter.pinned(true);
122 | statusFilter.defaultValue(['rejected', 'approved'])
123 |
124 | entity.listView().filters([statusFilter]);
125 |
126 | assert.equal(
127 | new Menu().populateFromEntity(entity).link(),
128 | '/comments/list?search=%7B%22status%22%3A%5B%22rejected%22%2C%22approved%22%5D%7D'
129 | );
130 | });
131 | });
132 |
133 | });
134 |
--------------------------------------------------------------------------------
/src/Entity/Entity.js:
--------------------------------------------------------------------------------
1 | import stringUtils from "../Utils/stringUtils";
2 | import Field from "../Field/Field";
3 | import DashboardView from '../View/DashboardView';
4 | import MenuView from '../View/MenuView';
5 | import ListView from '../View/ListView';
6 | import CreateView from '../View/CreateView';
7 | import EditView from '../View/EditView';
8 | import DeleteView from '../View/DeleteView';
9 | import ShowView from '../View/ShowView';
10 | import BatchDeleteView from '../View/BatchDeleteView';
11 | import ExportView from '../View/ExportView';
12 |
13 | var index = 0;
14 |
15 | class Entity {
16 | constructor(name) {
17 | this._name = name;
18 | this._uniqueId = this._name + '_' + index++;
19 | this._baseApiUrl = null;
20 | this._label = null;
21 | this._identifierField = new Field("id");
22 | this._isReadOnly = false;
23 | this._errorMessage = null;
24 | this._order = 0;
25 | this._url = null;
26 | this._createMethod = null; // manually set the HTTP-method for create operation, defaults to post
27 | this._updateMethod = null; // manually set the HTTP-method for update operation, defaults to put
28 | this._retrieveMethod = null; // manually set the HTTP-method for the get operation, defaults to get
29 | this._deleteMethod = null; // manually set the HTTP-method for the delete operation, defaults to delete
30 |
31 |
32 | this._initViews();
33 | }
34 |
35 | get uniqueId() {
36 | return this._uniqueId;
37 | }
38 |
39 | get views() {
40 | return this._views;
41 | }
42 |
43 | label() {
44 | if (arguments.length) {
45 | this._label = arguments[0];
46 | return this;
47 | }
48 |
49 | if (this._label === null) {
50 | return stringUtils.camelCase(this._name);
51 | }
52 |
53 | return this._label;
54 | }
55 |
56 | name() {
57 | if (arguments.length) {
58 | this._name = arguments[0];
59 | return this;
60 | }
61 |
62 | return this._name;
63 | }
64 |
65 | menuView() {
66 | return this._views["MenuView"];
67 | }
68 |
69 | dashboardView() {
70 | return this._views["DashboardView"];
71 | }
72 |
73 | listView() {
74 | return this._views["ListView"];
75 | }
76 |
77 | creationView() {
78 | return this._views["CreateView"];
79 | }
80 |
81 | editionView() {
82 | return this._views["EditView"];
83 | }
84 |
85 | deletionView() {
86 | return this._views["DeleteView"];
87 | }
88 |
89 | batchDeleteView() {
90 | return this._views["BatchDeleteView"];
91 | }
92 |
93 | exportView() {
94 | return this._views["ExportView"];
95 | }
96 |
97 | showView() {
98 | return this._views["ShowView"];
99 | }
100 |
101 | baseApiUrl(baseApiUrl) {
102 | if (!arguments.length) return this._baseApiUrl;
103 | this._baseApiUrl = baseApiUrl;
104 | return this;
105 | }
106 |
107 | _initViews() {
108 | this._views = {
109 | "DashboardView": new DashboardView().setEntity(this),
110 | "MenuView": new MenuView().setEntity(this),
111 | "ListView": new ListView().setEntity(this),
112 | "CreateView": new CreateView().setEntity(this),
113 | "EditView": new EditView().setEntity(this),
114 | "DeleteView": new DeleteView().setEntity(this),
115 | "BatchDeleteView": new BatchDeleteView().setEntity(this),
116 | "ExportView": new ExportView().setEntity(this),
117 | "ShowView": new ShowView().setEntity(this)
118 | };
119 | }
120 |
121 | identifier(value) {
122 | if (!arguments.length) return this._identifierField;
123 | if (!(value instanceof Field)) {
124 | throw new Error('Entity ' + this.name() + ': identifier must be an instance of Field.');
125 | }
126 | this._identifierField = value;
127 | return this;
128 | }
129 |
130 | readOnly() {
131 | this._isReadOnly = true;
132 |
133 | this._views["CreateView"].disable();
134 | this._views["EditView"].disable();
135 | this._views["DeleteView"].disable();
136 | this._views["BatchDeleteView"].disable();
137 |
138 | return this;
139 | }
140 |
141 | get isReadOnly() {
142 | return this._isReadOnly;
143 | }
144 |
145 | getErrorMessage(response) {
146 | if (typeof(this._errorMessage) === 'function') {
147 | return this._errorMessage(response);
148 | }
149 |
150 | return this._errorMessage;
151 | }
152 |
153 | errorMessage(errorMessage) {
154 | if (!arguments.length) return this._errorMessage;
155 | this._errorMessage = errorMessage;
156 | return this;
157 | }
158 |
159 | order(order) {
160 | if (!arguments.length) return this._order;
161 | this._order = order;
162 | return this;
163 | }
164 |
165 | url(url) {
166 | if (!arguments.length) return this._url;
167 | this._url = url;
168 | return this;
169 | }
170 |
171 | getUrl(viewType, identifierValue, identifierName) {
172 | if (typeof(this._url) === 'function') {
173 | return this._url(this.name(), viewType, identifierValue, identifierName);
174 | }
175 |
176 | return this._url;
177 | }
178 |
179 | createMethod(createMethod) {
180 | if (!arguments.length) return this._createMethod;
181 | this._createMethod = createMethod;
182 | return this;
183 | }
184 |
185 | updateMethod(updateMethod) {
186 | if (!arguments.length) return this._updateMethod;
187 | this._updateMethod = updateMethod;
188 | return this;
189 | }
190 |
191 | retrieveMethod(retrieveMethod) {
192 | if (!arguments.length) return this._retrieveMethod;
193 | this._retrieveMethod = retrieveMethod;
194 | return this;
195 | }
196 |
197 | deleteMethod(deleteMethod) {
198 | if (!arguments.length) return this._deleteMethod;
199 | this._deleteMethod = deleteMethod;
200 | return this;
201 | }
202 | }
203 |
204 | export default Entity;
205 |
--------------------------------------------------------------------------------
/tests/lib/Field/FieldTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Entry from "../../../lib/Entry";
4 | import Field from "../../../lib/Field/Field";
5 |
6 | describe('Field', function() {
7 | describe('detailLink', function() {
8 | it('should be false if not specified', function() {
9 | var field = new Field('foo');
10 | assert.equal(false, field.isDetailLink());
11 | });
12 |
13 | it('should be true if not specified and if name is "id"', function() {
14 | var field = new Field('id');
15 | assert.equal(true, field.isDetailLink());
16 | });
17 |
18 | it('should return given value if already set, whatever name may be', function() {
19 | var field = new Field('id');
20 | field.detailLink = false;
21 |
22 | assert.equal(false, field.isDetailLink());
23 | });
24 | });
25 |
26 | describe('label', function() {
27 | it('should be based on name if non label has been provided', function() {
28 | var field = new Field('first_name');
29 | assert.equal('First Name', field.label());
30 | });
31 |
32 | it('should return the camelCased name by default', function () {
33 | assert.equal(new Field('myField').label(), 'MyField');
34 | assert.equal(new Field('my_field_1').label(), 'My Field 1');
35 | assert.equal(new Field('my-field-2').label(), 'My Field 2');
36 | assert.equal(new Field('my_field-3').label(), 'My Field 3');
37 | });
38 |
39 | it('should be given value if already provided', function() {
40 | var field = new Field('first_name').label('Prénom');
41 | assert.equal('Prénom', field.label());
42 | });
43 | });
44 |
45 | describe('getCssClasses', function() {
46 | var field;
47 |
48 | beforeEach(function() {
49 | field = new Field('title');
50 | });
51 |
52 | it('should return result of callback called with entry if function', function() {
53 | field.cssClasses(entry => entry.values.id % 2 ? "odd-entry" : "even-entry");
54 |
55 | var entry = new Entry('post', { id: 1 });
56 | assert.equal("odd-entry", field.getCssClasses(entry));
57 |
58 | entry = new Entry('post', { id: 2 });
59 | assert.equal("even-entry", field.getCssClasses(entry));
60 | });
61 |
62 | it('should concatenate all elements if array', function() {
63 | field.cssClasses(["important", "approved"]);
64 | assert.equal("important approved", field.getCssClasses());
65 | });
66 |
67 | it('should return passed classes if neither function nor array', function() {
68 | field.cssClasses("important approved");
69 | assert.equal("important approved", field.getCssClasses());
70 | });
71 |
72 | it('should return an empty string by default', function() {
73 | assert.equal(field.getCssClasses(), '');
74 | });
75 |
76 | it('should return an class string as set by cssClasses(string)', function() {
77 | field.cssClasses('foo bar');
78 | assert.equal(field.getCssClasses(), 'foo bar');
79 | });
80 |
81 | it('should return an class string as set by cssClasses(array)', function() {
82 | field.cssClasses(['foo', 'bar']);
83 | assert.equal(field.getCssClasses(), 'foo bar');
84 | });
85 |
86 | it('should return an class string as set by cssClasses(function)', function() {
87 | field.cssClasses(function() { return 'foo bar'; });
88 | assert.equal(field.getCssClasses(), 'foo bar');
89 | });
90 |
91 | });
92 |
93 | describe('validation()', function() {
94 | it('should have sensible defaults', function() {
95 | assert.deepEqual(new Field().validation(), {required: false, minlength : 0, maxlength : 99999});
96 | });
97 |
98 | it('should allow to override parts of the validation settings', function() {
99 | var field = new Field().validation({ required: true });
100 | assert.deepEqual(field.validation(), {required: true, minlength : 0, maxlength : 99999});
101 | });
102 |
103 | it('should allow to remove parts of the validation settings', function() {
104 | var field = new Field().validation({ minlength: null });
105 | assert.deepEqual(field.validation(), {required: false, maxlength : 99999});
106 | });
107 | });
108 |
109 | describe('map()', function() {
110 | it('should add a map function', function() {
111 | var fooFunc = function(a) { return a; }
112 | var field = new Field().map(fooFunc);
113 | assert.ok(field.hasMaps());
114 | assert.deepEqual(field.map(), [fooFunc]);
115 | });
116 | it('should allow multiple calls', function() {
117 | var fooFunc = function(a) { return a; }
118 | var barFunc = function(a) { return a + 1; }
119 | var field = new Field().map(fooFunc).map(barFunc);
120 | assert.deepEqual(field.map(), [fooFunc, barFunc]);
121 | });
122 | });
123 |
124 | describe('getMappedValue()', function() {
125 | it('should return the value argument if no maps', function() {
126 | var field = new Field();
127 | assert.equal(field.getMappedValue('foobar'), 'foobar');
128 | });
129 | it('should return the passed transformed by maps', function() {
130 | var field = new Field()
131 | .map(function add1(a) { return a + 1; })
132 | .map(function times2(a) { return a * 2; });
133 | assert.equal(field.getMappedValue(3), 8);
134 | });
135 | });
136 |
137 | describe('template()', function() {
138 | it('should accept string values', function () {
139 | var field = new Field().template('hello!');
140 | assert.equal(field.getTemplateValue(), 'hello!');
141 | });
142 |
143 | it('should accept function values', function () {
144 | var field = new Field().template(function () { return 'hello function !'; });
145 | assert.equal(field.getTemplateValue(), 'hello function !');
146 | });
147 | });
148 |
149 | describe('getTemplateValue()', function() {
150 | it('should return the template function executed with the supplied data', function() {
151 | var field = new Field().template(function (name) { return 'hello ' + name + ' !'; });
152 | assert.equal(field.getTemplateValue('John'), 'hello John !');
153 | });
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/src/Field/Field.js:
--------------------------------------------------------------------------------
1 | import stringUtils from "../Utils/stringUtils";
2 |
3 | class Field {
4 | constructor(name) {
5 | this._name = name || Math.random().toString(36).substring(7);
6 | this._detailLink = (name === 'id');
7 | this._type = "string";
8 | this._order = null;
9 | this._label = null;
10 | this._maps = [];
11 | this._transforms = [];
12 | this._attributes = {};
13 | this._cssClasses = null;
14 | this._validation = { required: false, minlength : 0, maxlength : 99999 };
15 | this._defaultValue = null;
16 | this._editable = true;
17 | this._sortable = true;
18 | this._detailLinkRoute = 'edit';
19 | this._pinned = false;
20 | this._flattenable = true;
21 | this._helpText = null;
22 | this.dashboard = true;
23 | this.list = true;
24 | this._template = () => '';
25 | this._templateIncludesLabel = false;
26 | }
27 |
28 | label() {
29 | if (arguments.length) {
30 | this._label = arguments[0];
31 | return this;
32 | }
33 |
34 | if (this._label === null) {
35 | return stringUtils.camelCase(this._name);
36 | }
37 |
38 | return this._label;
39 | }
40 |
41 | type() {
42 | return this._type;
43 | }
44 |
45 | name() {
46 | if (arguments.length) {
47 | this._name = arguments[0];
48 | return this;
49 | }
50 |
51 | return this._name;
52 | }
53 |
54 | order() {
55 | if (arguments.length) {
56 | if(arguments[1] !== true) {
57 | console.warn('Setting order with Field.order is deprecated, order directly in fields array');
58 | }
59 | this._order = arguments[0];
60 | return this;
61 | }
62 |
63 | return this._order;
64 | }
65 |
66 | isDetailLink(detailLink) {
67 | if (arguments.length) {
68 | this._detailLink = arguments[0];
69 | return this;
70 | }
71 |
72 | if (this._detailLink === null) {
73 | return this._name === 'id';
74 | }
75 |
76 | return this._detailLink;
77 | }
78 |
79 | set detailLink(isDetailLink) {
80 | return this._detailLink = isDetailLink;
81 | }
82 |
83 | /**
84 | * Add a function to be applied to the response object to turn it into an entry
85 | */
86 | map(fn) {
87 | if (!fn) return this._maps;
88 | if (typeof(fn) !== "function") {
89 | let type = typeof(fn);
90 | throw new Error(`Map argument should be a function, ${type} given.`);
91 | }
92 |
93 | this._maps.push(fn);
94 |
95 | return this;
96 | }
97 |
98 | hasMaps() {
99 | return !!this._maps.length;
100 | }
101 |
102 | getMappedValue(value, entry) {
103 | for (let i in this._maps) {
104 | value = this._maps[i](value, entry);
105 | }
106 |
107 | return value;
108 | }
109 |
110 | /**
111 | * Add a function to be applied to the entry to turn it into a response object
112 | */
113 | transform(fn) {
114 | if (!fn) return this._transforms;
115 | if (typeof(fn) !== "function") {
116 | let type = typeof(fn);
117 | throw new Error(`transform argument should be a function, ${type} given.`);
118 | }
119 |
120 | this._transforms.push(fn);
121 |
122 | return this;
123 | }
124 |
125 | hasTranforms() {
126 | return !!this._transforms.length;
127 | }
128 |
129 | getTransformedValue(value, entry) {
130 | for (let i in this._transforms) {
131 | value = this._transforms[i](value, entry);
132 | }
133 |
134 | return value;
135 | }
136 |
137 | attributes(attributes) {
138 | if (!arguments.length) {
139 | return this._attributes;
140 | }
141 |
142 | this._attributes = attributes;
143 |
144 | return this;
145 | }
146 |
147 | cssClasses(classes) {
148 | if (!arguments.length) return this._cssClasses;
149 | this._cssClasses = classes;
150 | return this;
151 | }
152 |
153 | getCssClasses(entry) {
154 | if (!this._cssClasses) {
155 | return '';
156 | }
157 |
158 | if (this._cssClasses.constructor === Array) {
159 | return this._cssClasses.join(' ');
160 | }
161 |
162 | if (typeof(this._cssClasses) === 'function') {
163 | return this._cssClasses(entry);
164 | }
165 |
166 | return this._cssClasses;
167 | }
168 |
169 | validation(validation) {
170 | if (!arguments.length) {
171 | return this._validation;
172 | }
173 |
174 | for (let property in validation) {
175 | if (!validation.hasOwnProperty(property)) continue;
176 | if (validation[property] === null) {
177 | delete this._validation[property];
178 | } else {
179 | this._validation[property] = validation[property];
180 | }
181 | }
182 |
183 | return this;
184 | }
185 |
186 | defaultValue(defaultValue) {
187 | if (!arguments.length) return this._defaultValue;
188 | this._defaultValue = defaultValue;
189 | return this;
190 | }
191 |
192 | editable(editable) {
193 | if (!arguments.length) return this._editable;
194 | this._editable = editable;
195 | return this;
196 | }
197 |
198 | sortable(sortable) {
199 | if (!arguments.length) return this._sortable;
200 | this._sortable = sortable;
201 | return this;
202 | }
203 |
204 | detailLinkRoute(route) {
205 | if (!arguments.length) return this._detailLinkRoute;
206 | this._detailLinkRoute = route;
207 | return this;
208 | }
209 |
210 | pinned(pinned) {
211 | if (!arguments.length) return this._pinned;
212 | this._pinned = pinned;
213 | return this;
214 | }
215 |
216 | flattenable() {
217 | return this._flattenable;
218 | }
219 |
220 | helpText() {
221 | if (arguments.length) {
222 | this._helpText = arguments[0];
223 | return this;
224 | }
225 |
226 | return this._helpText;
227 | }
228 |
229 | getTemplateValue(data) {
230 | if (typeof(this._template) === 'function') {
231 | return this._template(data);
232 | }
233 |
234 | return this._template;
235 | }
236 |
237 | getTemplateValueWithLabel(data) {
238 | return this._templateIncludesLabel ? this.getTemplateValue(data) : false;
239 | }
240 |
241 | templateIncludesLabel(templateIncludesLabel) {
242 | if (!arguments.length) return this._templateIncludesLabel;
243 | this._templateIncludesLabel = templateIncludesLabel;
244 | return this;
245 | }
246 |
247 | template(template, templateIncludesLabel = false) {
248 | if (!arguments.length) return this._template;
249 | this._template = template;
250 | this._templateIncludesLabel = templateIncludesLabel;
251 | return this;
252 | }
253 | }
254 |
255 | export default Field;
256 |
--------------------------------------------------------------------------------
/src/Application.js:
--------------------------------------------------------------------------------
1 | import Menu from './Menu/Menu';
2 | import Collection from './Collection';
3 | import Dashboard from './Dashboard';
4 | import orderElement from "./Utils/orderElement";
5 |
6 | class Application {
7 | constructor(title='ng-admin', debug=true) {
8 | this._baseApiUrl = '';
9 | this._customTemplate = function(viewName) {};
10 | this._title = title;
11 | this._menu = null;
12 | this._dashboard = null;
13 | this._layout = false;
14 | this._header = false;
15 | this._entities = [];
16 | this._errorMessage = this.defaultErrorMessage;
17 | this._debug = debug;
18 | }
19 |
20 | defaultErrorMessage(response) {
21 | let body = response.data;
22 |
23 | if (typeof body === 'object') {
24 | body = JSON.stringify(body);
25 | }
26 |
27 | return 'Oops, an error occured : (code: ' + response.status + ') ' + body;
28 | }
29 |
30 | get entities() {
31 | return this._entities;
32 | }
33 |
34 | getViewsOfType(type) {
35 | return orderElement.order(
36 | this.entities.map(entity => entity.views[type])
37 | .filter(view => view.enabled)
38 | );
39 | }
40 |
41 | getRouteFor(entity, viewUrl, viewType, identifierValue, identifierName) {
42 | let baseApiUrl = entity.baseApiUrl() || this.baseApiUrl(),
43 | url = viewUrl || entity.getUrl(viewType, identifierValue, identifierName);
44 |
45 | // If the view or the entity don't define the url, retrieve it from the baseURL of the entity or the app
46 | if (!url) {
47 | url = baseApiUrl + encodeURIComponent(entity.name());
48 | if (identifierValue != null) {
49 | url += '/' + encodeURIComponent(identifierValue);
50 | }
51 | } else if (!/^(?:[a-z]+:)?\/\//.test(url)) {
52 | // Add baseUrl for relative URL
53 | url = baseApiUrl + url;
54 | }
55 |
56 | return url;
57 | }
58 |
59 | debug(debug) {
60 | if (!arguments.length) return this._debug;
61 | this._debug = debug;
62 | return this;
63 | }
64 |
65 | layout(layout) {
66 | if (!arguments.length) return this._layout;
67 | this._layout = layout;
68 | return this;
69 | }
70 |
71 | header(header) {
72 | if (!arguments.length) return this._header;
73 | this._header = header;
74 | return this;
75 | }
76 |
77 | title(title) {
78 | if (!arguments.length) return this._title;
79 | this._title = title;
80 | return this;
81 | }
82 |
83 | /**
84 | * Getter/Setter for the main application menu
85 | *
86 | * If the getter is called first, it will return a menu based on entities.
87 | *
88 | * application.addEntity(new Entity('posts'));
89 | * application.addEntity(new Entity('comments'));
90 | * application.menu(); // Menu { children: [ Menu { title: "Posts" }, Menu { title: "Comments" } ]}
91 | *
92 | * If the setter is called first, all subsequent calls to the getter will return the set menu.
93 | *
94 | * application.addEntity(new Entity('posts'));
95 | * application.addEntity(new Entity('comments'));
96 | * application.menu(new Menu().addChild(new Menu().title('Foo')));
97 | * application.menu(); // Menu { children: [ Menu { title: "Foo" } ]}
98 | *
99 | * @see Menu
100 | */
101 | menu(menu) {
102 | if (!arguments.length) {
103 | if (!this._menu) {
104 | this._menu = this.buildMenuFromEntities();
105 | }
106 | return this._menu
107 | }
108 |
109 | this._menu = menu;
110 | return this;
111 | }
112 |
113 | buildMenuFromEntities() {
114 | return new Menu().children(
115 | this.entities
116 | .filter(entity => entity.menuView().enabled)
117 | .sort((e1, e2) => e1.menuView().order() - e2.menuView().order())
118 | .map(entity => new Menu().populateFromEntity(entity))
119 | );
120 | }
121 |
122 | dashboard(dashboard) {
123 | if (!arguments.length) {
124 | if (!this._dashboard) {
125 | this._dashboard = this.buildDashboardFromEntities();
126 | }
127 | return this._dashboard
128 | }
129 | this._dashboard = dashboard;
130 | return this;
131 | }
132 |
133 | buildDashboardFromEntities() {
134 | let dashboard = new Dashboard()
135 | this.entities
136 | .filter(entity => entity.dashboardView().enabled)
137 | .map(entity => {
138 | dashboard.addCollection(entity.dashboardView()); // yep, a collection is a ListView, and so is a DashboardView - forgive this duck typing for BC sake
139 | });
140 | if (!dashboard.hasCollections()) {
141 | // still no collection from dashboardViews, let's use listViews instead
142 | this.entities
143 | .filter(entity => entity.listView().enabled)
144 | .map((entity, index) => {
145 | let collection = new Collection();
146 | let listView = entity.listView();
147 | collection.setEntity(entity);
148 | collection.perPage(listView.perPage())
149 | collection.sortField(listView.sortField())
150 | collection.sortDir(listView.sortDir())
151 | collection.order(index);
152 | // use only the first 3 cols
153 | collection.fields(listView.fields().filter((el, index) => index < 3));
154 | dashboard.addCollection(collection);
155 | });
156 | }
157 | return dashboard;
158 | }
159 |
160 | customTemplate(customTemplate) {
161 | if (!arguments.length) return this._customTemplate;
162 | this._customTemplate = customTemplate;
163 | return this;
164 | }
165 |
166 | baseApiUrl(url) {
167 | if (!arguments.length) return this._baseApiUrl;
168 | this._baseApiUrl = url;
169 | return this;
170 | }
171 |
172 | addEntity(entity) {
173 | if (!entity) {
174 | throw new Error("No entity given");
175 | }
176 |
177 | this._entities.push(entity);
178 |
179 | return this;
180 | }
181 |
182 | getEntity(entityName) {
183 | let foundEntity = this._entities.filter(e => e.name() === entityName)[0];
184 | if (!foundEntity) {
185 | throw new Error(`Unable to find entity "${entityName}"`);
186 | }
187 |
188 | return foundEntity;
189 | }
190 |
191 | hasEntity(fieldName) {
192 | return !!(this._entities.filter(f => f.name() === fieldName).length);
193 | }
194 |
195 | getViewByEntityAndType(entityName, type) {
196 | return this._entities
197 | .filter(e => e.name() === entityName)[0]
198 | .views[type];
199 | }
200 |
201 | getErrorMessage(response) {
202 | if (typeof(this._errorMessage) === 'function') {
203 | return this._errorMessage(response);
204 | }
205 |
206 | return this._errorMessage;
207 | }
208 |
209 | errorMessage(errorMessage) {
210 | if (!arguments.length) return this._errorMessage;
211 | this._errorMessage = errorMessage;
212 | return this;
213 | }
214 |
215 | getErrorMessageFor(view, response) {
216 | return (
217 | view.getErrorMessage(response)
218 | || view.getEntity().getErrorMessage(response)
219 | || this.getErrorMessage(response)
220 | );
221 | }
222 |
223 | getEntityNames() {
224 | return this.entities.map(f => f.name());
225 | }
226 | }
227 |
228 | export default Application;
229 |
--------------------------------------------------------------------------------
/src/View/View.js:
--------------------------------------------------------------------------------
1 | import Entry from '../Entry';
2 | import ReferenceExtractor from '../Utils/ReferenceExtractor';
3 | import { clone, cloneAndFlatten, cloneAndNest } from '../Utils/objectProperties';
4 |
5 | class View {
6 | constructor(name) {
7 | this.entity = null;
8 | this._actions = null;
9 | this._title = false;
10 | this._description = '';
11 | this._template = null;
12 |
13 | this._enabled = null;
14 | this._fields = [];
15 | this._type = null;
16 | this._name = name;
17 | this._order = 0;
18 | this._errorMessage = null;
19 | this._url = null;
20 | this._prepare = null;
21 | }
22 |
23 | get enabled() {
24 | return this._enabled === null ? !!this._fields.length : this._enabled;
25 | }
26 |
27 | title(title) {
28 | if (!arguments.length) return this._title;
29 | this._title = title;
30 | return this;
31 | }
32 |
33 | description() {
34 | if (arguments.length) {
35 | this._description = arguments[0];
36 | return this;
37 | }
38 |
39 | return this._description;
40 | }
41 |
42 | name(name) {
43 | if (!arguments.length) {
44 | return this._name || this.entity.name() + '_' + this._type;
45 | }
46 |
47 | this._name = name;
48 | return this;
49 | }
50 |
51 | disable() {
52 | this._enabled = false;
53 |
54 | return this;
55 | }
56 |
57 | enable() {
58 | this._enabled = true;
59 |
60 | return this;
61 | }
62 |
63 | /**
64 | * @deprecated Use getter "enabled" instead
65 | */
66 | isEnabled() {
67 | return this.enabled;
68 | }
69 |
70 | /**
71 | * @deprecated Use getter "entity" instead
72 | */
73 | getEntity() {
74 | return this.entity;
75 | }
76 |
77 | /**
78 | * @deprecated Specify entity at view creation or use "entity" setter instead
79 | */
80 | setEntity(entity) {
81 | this.entity = entity;
82 | if (!this._name) {
83 | this._name = entity.name() + '_' + this._type;
84 | }
85 |
86 | return this;
87 | }
88 |
89 | /*
90 | * Supports various syntax
91 | * fields([ Field1, Field2 ])
92 | * fields(Field1, Field2)
93 | * fields([Field1, {Field2, Field3}])
94 | * fields(Field1, {Field2, Field3})
95 | * fields({Field2, Field3})
96 | */
97 | fields() {
98 | if (!arguments.length) return this._fields;
99 |
100 | [].slice.call(arguments).map(function(argument) {
101 | View.flatten(argument).map(arg => this.addField(arg));
102 | }, this);
103 |
104 | return this;
105 | }
106 |
107 | hasFields() {
108 | return this.fields.length > 0;
109 | }
110 |
111 | removeFields() {
112 | this._fields = [];
113 | return this;
114 | }
115 |
116 | getFields() {
117 | return this._fields;
118 | }
119 |
120 | getField(fieldName) {
121 | return this._fields.filter(f => f.name() === fieldName)[0];
122 | }
123 |
124 | getFieldsOfType(type) {
125 | return this._fields.filter(f => f.type() === type);
126 | }
127 |
128 | addField(field) {
129 | if (field.order() === null) {
130 | field.order(this._fields.length, true);
131 | }
132 | this._fields.push(field);
133 | this._fields = this._fields.sort((a, b) => (a.order() - b.order()));
134 |
135 | return this;
136 | }
137 |
138 | static flatten(arg) {
139 | if (arg.constructor.name === 'Object') {
140 | console.warn('Passing literal of Field to fields method is deprecated use array instead');
141 | let result = [];
142 | for (let fieldName in arg) {
143 | result = result.concat(View.flatten(arg[fieldName]));
144 | }
145 | return result;
146 | }
147 | if (Array.isArray(arg)) {
148 | return arg.reduce(function(previous, current) {
149 | return previous.concat(View.flatten(current))
150 | }, []);
151 | }
152 | // arg is a scalar
153 | return [arg];
154 | }
155 |
156 | get type() {
157 | return this._type;
158 | }
159 |
160 | order(order) {
161 | if (!arguments.length) return this._order;
162 | this._order = order;
163 | return this;
164 | }
165 |
166 | getReferences(withRemoteComplete) {
167 | return ReferenceExtractor.getReferences(this._fields, withRemoteComplete);
168 | }
169 |
170 | getNonOptimizedReferences(withRemoteComplete) {
171 | return ReferenceExtractor.getNonOptimizedReferences(this._fields, withRemoteComplete);
172 | }
173 |
174 | getOptimizedReferences(withRemoteComplete) {
175 | return ReferenceExtractor.getOptimizedReferences(this._fields, withRemoteComplete);
176 | }
177 |
178 | getReferencedLists() {
179 | return ReferenceExtractor.getReferencedLists(this._fields);
180 | }
181 |
182 | template(template) {
183 | if (!arguments.length) {
184 | return this._template;
185 | }
186 |
187 | this._template = template;
188 |
189 | return this;
190 | }
191 |
192 | identifier() {
193 | return this.entity.identifier();
194 | }
195 |
196 | actions(actions) {
197 | if (!arguments.length) return this._actions;
198 | this._actions = actions;
199 | return this;
200 | }
201 |
202 | getErrorMessage(response) {
203 | if (typeof(this._errorMessage) === 'function') {
204 | return this._errorMessage(response);
205 | }
206 |
207 | return this._errorMessage;
208 | }
209 |
210 | errorMessage(errorMessage) {
211 | if (!arguments.length) return this._errorMessage;
212 | this._errorMessage = errorMessage;
213 | return this;
214 | }
215 |
216 | url(url) {
217 | if (!arguments.length) return this._url;
218 | this._url = url;
219 | return this;
220 | }
221 |
222 | getUrl(identifierValue) {
223 | if (typeof(this._url) === 'function') {
224 | return this._url(identifierValue);
225 | }
226 |
227 | return this._url;
228 | }
229 |
230 | validate(entry) {
231 | this._fields.map(function (field) {
232 | let validation = field.validation();
233 |
234 | if (typeof validation.validator === 'function') {
235 | validation.validator(entry.values[field.name()], entry.values);
236 | }
237 | });
238 | }
239 |
240 | /**
241 | * Map a JS object from the REST API Response to an Entry
242 | */
243 | mapEntry(restEntry) {
244 | return Entry.createFromRest(restEntry, this._fields, this.entity.name(), this.entity.identifier().name());
245 | }
246 |
247 | mapEntries(restEntries) {
248 | return Entry.createArrayFromRest(restEntries, this._fields, this.entity.name(), this.entity.identifier().name());
249 | }
250 |
251 | /**
252 | * Transform an Entry to a JS object for the REST API Request
253 | */
254 | transformEntry(entry) {
255 | return entry.transformToRest(this._fields);
256 | }
257 |
258 | /**
259 | * Add a function to be executed before the view renders
260 | *
261 | * This is the ideal place to prefetch related entities and manipulate
262 | * the dataStore.
263 | *
264 | * The syntax depends on the framework calling the function.
265 | *
266 | * With ng-admin, the function can be an angular injectable, listing
267 | * required dependencies in an array. Among other, the function can receive
268 | * the following services:
269 | * - query: the query object (an object representation of the main request
270 | * query string)
271 | * - datastore: where the Entries are stored. The dataStore is accessible
272 | * during rendering
273 | * - view: the current View object
274 | * - entry: the current Entry instance (except in listView)
275 | * - Entry: the Entry constructor (required to transform an object from
276 | * the REST response to an Entry)
277 | * - window: the window object. If you need to fetch anything other than an
278 | * entry and pass it to the view layer, it's the only way.
279 | *
280 | * The function can be asynchronous, in which case it should return
281 | * a Promise.
282 | *
283 | * @example
284 | *
285 | * post.listView().prepare(['datastore', 'view', 'Entry', function(datastore, view, Entry) {
286 | * const posts = datastore.getEntries(view.getEntity().uniqueId);
287 | * const authorIds = posts.map(post => post.values.authorId).join(',');
288 | * return fetch('http://myapi.com/authors?id[]=' + authorIds)
289 | * .then(response => response.json())
290 | * .then(authors => Entry.createArrayFromRest(
291 | * authors,
292 | * [new Field('first_name'), new Field('last_name')],
293 | * 'author'
294 | * ))
295 | * .then(authorEntries => datastore.setEntries('authors', authorEntries));
296 | * }]);
297 | */
298 | prepare(prepare) {
299 | if (!arguments.length) return this._prepare;
300 | this._prepare = prepare;
301 | return this;
302 | }
303 |
304 | doPrepare() {
305 | return this._prepare.apply(this, arguments);
306 | }
307 | }
308 |
309 | export default View;
310 |
--------------------------------------------------------------------------------
/tests/lib/Queries/ReadQueriesTest.js:
--------------------------------------------------------------------------------
1 | let assert = require('chai').assert,
2 | sinon = require('sinon');
3 |
4 | import ReadQueries from "../../../lib/Queries/ReadQueries";
5 | import PromisesResolver from "../../mock/PromisesResolver";
6 | import Entity from "../../../lib/Entity/Entity";
7 | import ReferenceField from "../../../lib/Field/ReferenceField";
8 | import ReferencedListField from "../../../lib/Field/ReferencedListField";
9 | import TextField from "../../../lib/Field/TextField";
10 | import Field from "../../../lib/Field/Field";
11 | import buildPromise from "../../mock/mixins";
12 |
13 | describe('ReadQueries', () => {
14 | let readQueries,
15 | restWrapper = {},
16 | application = {},
17 | rawCats,
18 | rawHumans,
19 | catEntity,
20 | humanEntity,
21 | catView,
22 | humanView;
23 |
24 | beforeEach(() => {
25 | application = {
26 | getRouteFor: (entity, generatedUrl, viewType, id) => {
27 | let url = 'http://localhost/' + encodeURIComponent(entity.name());
28 | if (id) {
29 | url += '/' + encodeURIComponent(id);
30 | }
31 |
32 | return url;
33 | }
34 | };
35 |
36 | readQueries = new ReadQueries(restWrapper, PromisesResolver, application);
37 | catEntity = new Entity('cat');
38 | humanEntity = new Entity('human');
39 | humanView = humanEntity.listView()
40 | .fields([
41 | new Field('name'),
42 | new ReferencedListField('cat_id').targetEntity(catEntity).targetReferenceField('human_id')
43 | ]);
44 | catView = catEntity.listView()
45 | .addField(new TextField('name'))
46 | .addField(new ReferenceField('human_id').targetEntity(humanEntity).targetField(new Field('firstName')));
47 |
48 | humanEntity.identifier(new Field('id'));
49 |
50 | rawCats = [
51 | {"id": 1, "human_id": 1, "name": "Mizoute", "summary": "A Cat"},
52 | {"id": 2, "human_id": 1, "name": "Suna", "summary": "A little Cat"}
53 | ];
54 |
55 | rawHumans = [
56 | {"id": 1, "firstName": "Daph"},
57 | {"id": 2, "firstName": "Manu"},
58 | {"id": 3, "firstName": "Daniel"}
59 | ];
60 | });
61 |
62 | describe("getOne", () => {
63 |
64 | it('should return the entity with all fields.', () => {
65 | let entity = new Entity('cat');
66 | entity.views['ListView']
67 | .addField(new TextField('name'));
68 |
69 | restWrapper.getOne = sinon.stub().returns(buildPromise({
70 | data: {
71 | "id": 1,
72 | "name": "Mizoute",
73 | "summary": "A Cat"
74 | }
75 | }));
76 |
77 | readQueries.getOne(entity, 'list', 1)
78 | .then((rawEntry) => {
79 | assert(restWrapper.getOne.calledWith('cat', 'http://localhost/cat/1'));
80 |
81 | assert.equal(rawEntry.data.id, 1);
82 | assert.equal(rawEntry.data.name, 'Mizoute');
83 |
84 | // Non mapped field should also be retrieved
85 | assert.equal(rawEntry.data.summary, "A Cat");
86 | });
87 | });
88 |
89 | });
90 |
91 | describe('getAll', () => {
92 | it('should return all data to display a ListView', () => {
93 | restWrapper.getList = sinon.stub().returns(buildPromise({data: rawCats, headers: () => {}}));
94 | PromisesResolver.allEvenFailed = sinon.stub().returns(buildPromise([
95 | {status: 'success', result: rawHumans[0] },
96 | {status: 'success', result: rawHumans[1] },
97 | {status: 'success', result: rawHumans[2] }
98 | ]));
99 |
100 | readQueries.getAll(catView)
101 | .then((result) => {
102 | assert.equal(result.totalItems, 2);
103 | assert.equal(result.data.length, 2);
104 |
105 | assert.equal(result.data[0].id, 1);
106 | assert.equal(result.data[0].name, 'Mizoute');
107 |
108 | assert.equal(result.data[0].human_id, 1);
109 | });
110 | });
111 |
112 | it('should send correct page params to the API call', () => {
113 | let spy = sinon.spy(readQueries, 'getRawValues');
114 |
115 | readQueries.getAll(catView, 2);
116 |
117 | assert(spy.withArgs(catEntity, catView.name(), catView.type, 2).calledOnce);
118 | });
119 |
120 | it('should send correct sort params to the API call', () => {
121 | let spy = sinon.spy(readQueries, 'getRawValues');
122 | catView.sortField('name').sortDir('DESC');
123 | let viewName = catView.name();
124 |
125 | readQueries.getAll(catView, 1);
126 | readQueries.getAll(catView, 1, {}, 'unknow_ListView.name', 'ASC');
127 | readQueries.getAll(catView, 1, {}, viewName + '.id', 'ASC');
128 |
129 | assert(spy.withArgs(catEntity, viewName, catView.type, 1, catView.perPage(), {}, catView.filters(), viewName + '.name', 'DESC').callCount == 2);
130 | assert(spy.withArgs(catEntity, viewName, catView.type, 1, catView.perPage(), {}, catView.filters(), viewName + '.id', 'ASC').calledOnce);
131 | });
132 |
133 | it('should send correct filter params to the API call', () => {
134 | let spy = sinon.spy(readQueries, 'getRawValues');
135 | let entity = new Entity('cat');
136 |
137 | readQueries.getAll(entity.listView(), 1, { name: 'foo'});
138 |
139 | assert(spy.withArgs(entity, 'cat_ListView', 'ListView', 1, 30, { name: 'foo' }, [], 'cat_ListView.id', 'DESC').calledOnce);
140 | });
141 |
142 | it('should include permanent filters', () => {
143 | let spy = sinon.spy(readQueries, 'getRawValues');
144 | let entity = new Entity('cat');
145 | entity.listView().permanentFilters({ bar: 1 });
146 |
147 | readQueries.getAll(entity.listView(), 1);
148 | readQueries.getAll(entity.listView(), 1, { name: 'foo'});
149 |
150 | assert(spy.withArgs(entity, 'cat_ListView', 'ListView', 1, 30, { bar: 1 }, [], 'cat_ListView.id', 'DESC').calledOnce);
151 | assert(spy.withArgs(entity, 'cat_ListView', 'ListView', 1, 30, { name: 'foo', bar: 1 }, [], 'cat_ListView.id', 'DESC').calledOnce);
152 | });
153 |
154 | });
155 |
156 | describe('getReferencedData', () => {
157 | it('should return all references data for a View with multiple calls', () => {
158 | let post = new Entity('posts'),
159 | author = new Entity('authors'),
160 | authorRef = new ReferenceField('author');
161 |
162 | let rawPosts = [
163 | {id: 1, author: 'abc'},
164 | {id: 2, author: '19DFE'}
165 | ];
166 |
167 | let rawAuthors = [
168 | {id: 'abc', name: 'Rollo'},
169 | {id: '19DFE', name: 'Ragna'}
170 | ];
171 |
172 | authorRef.targetEntity(author);
173 | authorRef.targetField(new Field('name'));
174 | post.views["ListView"]
175 | .addField(authorRef);
176 |
177 | restWrapper.getOne = sinon.stub().returns(buildPromise({}));
178 | PromisesResolver.allEvenFailed = sinon.stub().returns(buildPromise([
179 | {status: 'success', result: rawAuthors[0] },
180 | { status: 'success', result: rawAuthors[1] }
181 | ]));
182 |
183 | readQueries.getFilteredReferenceData(post.views["ListView"].getReferences(), rawPosts)
184 | .then((referencedData) => {
185 | assert.equal(referencedData.author.length, 2);
186 | assert.equal(referencedData.author[0].id, 'abc');
187 | assert.equal(referencedData.author[1].name, 'Ragna');
188 | });
189 | });
190 |
191 | it('should return all references data for a View with one call', () => {
192 | let post = new Entity('posts'),
193 | author = new Entity('authors'),
194 | authorRef = new ReferenceField('author');
195 |
196 | authorRef.singleApiCall((ids) => {
197 | return {
198 | id: ids
199 | };
200 | });
201 |
202 | let rawPosts = [
203 | {id: 1, author: 'abc'},
204 | {id: 2, author: '19DFE'}
205 | ];
206 |
207 | let rawAuthors = [
208 | {id: 'abc', name: 'Rollo'},
209 | {id: '19DFE', name: 'Ragna'}
210 | ];
211 |
212 | authorRef.targetEntity(author);
213 | authorRef.targetField(new Field('name'));
214 | post.views["ListView"]
215 | .addField(authorRef);
216 |
217 | restWrapper.getList = sinon.stub().returns(buildPromise({data: rawCats, headers: () => {}}));
218 | PromisesResolver.allEvenFailed = sinon.stub().returns(buildPromise([
219 | {status: 'success', result: { data: rawAuthors }}
220 | ]));
221 |
222 | readQueries.getOptimizedReferenceData(post.views["ListView"].getReferences(), rawPosts)
223 | .then((referencedData) => {
224 | assert.equal(referencedData['author'].length, 2);
225 | assert.equal(referencedData['author'][0].id, 'abc');
226 | assert.equal(referencedData['author'][1].name, 'Ragna');
227 | });
228 | });
229 | });
230 |
231 | describe('getReferencedListData', () => {
232 | it('should return all referenced list data for a View', () => {
233 | restWrapper.getList = sinon.stub().returns(buildPromise({data: rawCats, headers: () => {}}));
234 | PromisesResolver.allEvenFailed = sinon.stub().returns(buildPromise([
235 | {status: 'success', result: { data: rawCats }}
236 | ]));
237 |
238 | readQueries.getReferencedListData(humanView.getReferencedLists(), null, null, 1)
239 | .then((referencedListEntries) => {
240 | assert.equal(referencedListEntries['cat_id'].length, 2);
241 | assert.equal(referencedListEntries['cat_id'][0].id, 1);
242 | assert.equal(referencedListEntries['cat_id'][1].name, 'Suna');
243 | });
244 | });
245 |
246 | it('should send correct sort params to the API call', () => {
247 | let spy = sinon.spy(readQueries, 'getRawValues');
248 | humanView.getReferencedLists()['cat_id'].sortField('name').sortDir('DESC');
249 | let viewName = catView.name();
250 | let perPage = humanView.getReferencedLists()['cat_id'].perPage();
251 | let targetEntity = humanView.getReferencedLists()['cat_id'].targetEntity();
252 |
253 | readQueries.getReferencedListData(humanView.getReferencedLists(), null, null, 1);
254 | readQueries.getReferencedListData(humanView.getReferencedLists(), 'unknow_ListView.name', 'ASC', 1);
255 | readQueries.getReferencedListData(humanView.getReferencedLists(), 'cat_ListView.id', 'ASC', 1);
256 |
257 | assert(spy.withArgs(catEntity, viewName, 'listView', 1, perPage, { 'human_id': 1 }, {}, viewName + '.name', 'DESC').calledTwice);
258 | assert(spy.withArgs(targetEntity, viewName, 'listView', 1, perPage, { 'human_id': 1 }, {}, viewName + '.id', 'ASC').calledOnce);
259 | });
260 |
261 | it('should append permanentFilters to the queryString', () => {
262 | let readQueries = new ReadQueries(restWrapper, PromisesResolver, application);
263 | const commentEntity = new Entity('comment');
264 | const postEntity = new Entity('post');
265 | const postView = postEntity.showView()
266 | .fields([
267 | new Field('name'),
268 | new ReferencedListField('comments')
269 | .targetEntity(commentEntity)
270 | .targetReferenceField('post_id')
271 | .permanentFilters({ foo: 'bar' })
272 | ]);
273 |
274 | const comments = [
275 | { id: 1, post_id: 1, summary: "A comment", foo: "bar" },
276 | { id: 2, post_id: 1, summary: "Another comment" }
277 | ];
278 | var post = { id: 1, title: "Hello, World" };
279 |
280 | var spy = sinon.stub(readQueries, 'getRawValues', function(entity, viewType, identifierValue) {
281 | return buildPromise({ result: post });
282 | });
283 | PromisesResolver.allEvenFailed = sinon.stub().returns(buildPromise([
284 | { status: 'success', result: { data: [comments[0]] } }
285 | ]));
286 |
287 | readQueries.getReferencedListData(postView.getReferencedLists(), null, null, 1)
288 | .then((referencedListEntries) => {
289 | let targetEntity = postView.getReferencedLists()['comments'].targetEntity();
290 | assert(spy.withArgs(commentEntity, 'comment_ListView', 'listView', 1, 30, { post_id: 1, foo: 'bar' }, {}).calledOnce);
291 | });
292 | });
293 | });
294 |
295 | describe('getAllReferencedData', () => {
296 | var getRawValuesMock;
297 | beforeEach(function() {
298 | getRawValuesMock = sinon.mock(readQueries);
299 | });
300 |
301 | it('should execute permanentFilters function with current search parameter if filters is a function', () => {
302 | var searchParameter = null;
303 | var field = new ReferenceField('myField')
304 | .targetEntity(humanEntity)
305 | .targetField(new Field('name'))
306 | .permanentFilters((search) => { searchParameter = search; return { filter: search }; });
307 |
308 | getRawValuesMock.expects('getRawValues').once();
309 | readQueries.getAllReferencedData([field], 'foo');
310 |
311 | getRawValuesMock.verify();
312 | assert.equal(searchParameter, 'foo');
313 | });
314 |
315 | afterEach(function() {
316 | getRawValuesMock.restore();
317 | })
318 | });
319 |
320 |
321 | describe('getRecordsByIds', function() {
322 | it('should return a promise with array of all retrieved records', function(done) {
323 | var spy = sinon.stub(readQueries, 'getOne', function(entity, viewType, identifierValue) {
324 | return buildPromise({ result: identifierValue });
325 | });
326 |
327 | readQueries.getRecordsByIds(humanEntity, [1, 2]).then(function(records) {
328 | assert.equal(spy.callCount, 2);
329 | assert.equal(spy.args[0][2], 1);
330 | assert.equal(spy.args[1][2], 2);
331 | done();
332 | });
333 | });
334 |
335 | it('should return promise with empty result if no ids are passed', function(done) {
336 | var spy = sinon.spy(readQueries, 'getRawValues');
337 |
338 | readQueries.getRecordsByIds(humanEntity, []).then(function(records) {
339 | assert.equal(spy.called, false);
340 | assert.equal(records, null);
341 | done();
342 | });
343 | });
344 | });
345 | });
346 |
--------------------------------------------------------------------------------
/src/Queries/ReadQueries.js:
--------------------------------------------------------------------------------
1 | import Queries from './Queries';
2 | import ReferenceExtractor from '../Utils/ReferenceExtractor';
3 |
4 | class ReadQueries extends Queries {
5 |
6 | /**
7 | * Get one entity
8 | *
9 | * @param {Entity} entity
10 | * @param {String} viewType
11 | * @param {mixed} identifierValue
12 | * @param {String} identifierName
13 | * @param {String} url
14 | *
15 | * @returns {promise} (list of fields (with their values if set) & the entity name, label & id-
16 | */
17 | getOne(entity, viewType, identifierValue, identifierName, url) {
18 | return this._restWrapper
19 | .getOne(entity.name(), this._application.getRouteFor(entity, url, viewType, identifierValue, identifierName), entity.retrieveMethod());
20 | }
21 |
22 | /**
23 | * Return the list of all object of entityName type
24 | * Get all the object from the API
25 | *
26 | * @param {ListView} view the view associated to the entity
27 | * @param {Number} page the page number
28 | * @param {Object} filterValues searchQuery to filter elements
29 | * @param {String} sortField the field to be sorted ex: entity.fieldName
30 | * @param {String} sortDir the direction of the sort
31 | *
32 | * @returns {promise} the entity config & the list of objects
33 | */
34 | getAll(view, page, filterValues, sortField, sortDir) {
35 | page = page || 1;
36 | filterValues = filterValues || {};
37 | let url = view.getUrl();
38 |
39 | if (sortField && sortField.split('.')[0] === view.name()) {
40 | sortField = sortField;
41 | sortDir = sortDir;
42 | } else {
43 | sortField = view.getSortFieldName();
44 | sortDir = view.sortDir();
45 | }
46 |
47 | let allFilterValues = {};
48 | const permanentFilters = view.permanentFilters();
49 | Object.keys(filterValues).forEach(key => {
50 | allFilterValues[key] = filterValues[key];
51 | });
52 | Object.keys(permanentFilters).forEach(key => {
53 | allFilterValues[key] = permanentFilters[key];
54 | });
55 |
56 | return this.getRawValues(view.entity, view.name(), view.type, page, view.perPage(), allFilterValues, view.filters(), sortField, sortDir, url)
57 | .then((values) => {
58 | return {
59 | data: values.data,
60 | totalItems: values.totalCount || values.headers('X-Total-Count') || values.data.length
61 | };
62 | });
63 | }
64 |
65 | /**
66 | * Return the list of all object of entityName type
67 | * Get all the object from the API
68 | *
69 | * @param {Entity} entity
70 | * @param {String} viewName
71 | * @param {String} viewType
72 | * @param {Number} page
73 | * @param {Number} perPage
74 | * @param {Object} filterValues
75 | * @param {Object} filterFields
76 | * @param {String} sortField
77 | * @param {String} sortDir
78 | * @param {String} url
79 | *
80 | * @returns {promise} the entity config & the list of objects
81 | */
82 | getRawValues(entity, viewName, viewType, page, perPage, filterValues, filterFields, sortField, sortDir, url) {
83 | let params = {};
84 |
85 | // Compute pagination
86 | if (page !== -1) {
87 | params._page = (typeof (page) === 'undefined') ? 1 : parseInt(page, 10);
88 | params._perPage = perPage;
89 | }
90 |
91 | // Compute sorting
92 | if (sortField && sortField.split('.')[0] === viewName) {
93 | params._sortField = sortField.substr(sortField.indexOf('.') + 1);
94 | params._sortDir = sortDir;
95 | }
96 |
97 | // Compute filtering
98 | if (filterValues && Object.keys(filterValues).length !== 0) {
99 | params._filters = {};
100 | let filterName, mappedValue;
101 | for (filterName in filterValues) {
102 | if (filterFields.hasOwnProperty(filterName) && filterFields[filterName].hasMaps()) {
103 | mappedValue = filterFields[filterName].getMappedValue(filterValues[filterName]);
104 | Object.keys(mappedValue).forEach(key => {
105 | params._filters[key] = mappedValue[key];
106 | })
107 | continue;
108 | }
109 |
110 | // It's weird to not map, but why not.
111 | params._filters[filterName] = filterValues[filterName];
112 | }
113 | }
114 |
115 | // Get grid data
116 | return this._restWrapper
117 | .getList(params, entity.name(), this._application.getRouteFor(entity, url, viewType), entity.retrieveMethod());
118 | }
119 |
120 | getReferenceData(references, rawValues) {
121 | var nonOptimizedReferencedData = this.getFilteredReferenceData(ReferenceExtractor.getNonOptimizedReferences(references), rawValues);
122 | var optimizedReferencedData = this.getOptimizedReferenceData(ReferenceExtractor.getOptimizedReferences(references), rawValues);
123 | return Promise.all([nonOptimizedReferencedData, optimizedReferencedData])
124 | .then((results) => {
125 | let data = {};
126 | let name;
127 | for (name in results[0]) {
128 | data[name] = results[0][name];
129 | }
130 | for (name in results[1]) {
131 | data[name] = results[1][name];
132 | }
133 | return data;
134 | })
135 | }
136 |
137 | /**
138 | * Returns all References for an entity with associated values [{targetEntity.identifier: targetLabel}, ...]
139 | * by calling the API for each entries
140 | *
141 | * @param {ReferenceField} references A hash of Reference and ReferenceMany objects
142 | * @param {Array} rawValues
143 | *
144 | * @returns {Promise}
145 | */
146 | getFilteredReferenceData(references, rawValues) {
147 | if (!references || !Object.keys(references).length) {
148 | return this._promisesResolver.empty({});
149 | }
150 |
151 | let getOne = this.getOne.bind(this),
152 | calls = [];
153 |
154 | for (let i in references) {
155 | let reference = references[i],
156 | targetEntity = reference.targetEntity(),
157 | identifiers = reference.getIdentifierValues(rawValues);
158 |
159 | for (let k in identifiers) {
160 | calls.push(getOne(targetEntity, 'listView', identifiers[k], reference.name()));
161 | }
162 | }
163 |
164 | return this.fillFilteredReferencedData(calls, references, rawValues);
165 | }
166 |
167 | /**
168 | * Returns all References for an entity with associated values [{targetEntity.identifier: targetLabel}, ...]
169 | * by calling the API once
170 | *
171 | * @param {[ReferenceField]} references A hash of Reference and ReferenceMany objects
172 | * @param {Array} rawValues
173 | *
174 | * @returns {Promise}
175 | */
176 | getOptimizedReferenceData(references, rawValues) {
177 | if (!references || !Object.keys(references).length) {
178 | return this._promisesResolver.empty({});
179 | }
180 |
181 | let getRawValues = this.getRawValues.bind(this),
182 | calls = [];
183 |
184 | for (let i in references) {
185 | let reference = references[i],
186 | targetEntity = reference.targetEntity(),
187 | identifiers = reference.getIdentifierValues(rawValues);
188 |
189 | // Check if we should retrieve values with 1 or multiple requests
190 | let singleCallFilters = reference.getSingleApiCall(identifiers);
191 | calls.push(getRawValues(targetEntity, targetEntity.name() + '_ListView', 'listView', 1, reference.perPage(), singleCallFilters, {}, reference.sortField(), reference.sortDir()));
192 | }
193 |
194 | return this.fillOptimizedReferencedData(calls, references);
195 | }
196 |
197 | /**
198 | * Returns all References for an entity with associated values [{targetEntity.identifier: targetLabel}, ...]
199 | * without filters on an entity
200 | *
201 | * @param {[ReferenceField]} references A hash of Reference and ReferenceMany objects
202 | *
203 | * @returns {Promise}
204 | */
205 | getAllReferencedData(references, search) {
206 | if (!references || !Object.keys(references).length) {
207 | return this._promisesResolver.empty({});
208 | }
209 |
210 | let calls = [],
211 | getRawValues = this.getRawValues.bind(this);
212 |
213 | for (let i in references) {
214 | let reference = references[i];
215 | let targetEntity = reference.targetEntity();
216 |
217 | const permanentFilters = reference.permanentFilters();
218 | let filterValues = permanentFilters || {};
219 |
220 | if (typeof(permanentFilters) === 'function') {
221 | console.warn('Reference.permanentFilters() called with a function is deprecated. Use the searchQuery option for remoteComplete() instead');
222 | filterValues = permanentFilters(search);
223 | }
224 |
225 | if (search) {
226 | // remote complete situation
227 | let options = reference.remoteCompleteOptions();
228 | if (options.searchQuery) {
229 | let filterValuesFromRemoteComplete = options.searchQuery(search);
230 | Object.keys(filterValuesFromRemoteComplete).forEach(key => {
231 | filterValues[key] = filterValuesFromRemoteComplete[key];
232 | })
233 | } else {
234 | // by default, filter the list by the referenceField name
235 | filterValues[reference.targetField().name()] = search;
236 | }
237 | }
238 |
239 | let filterFields = {};
240 | filterFields[reference.name()] = reference;
241 |
242 | calls.push(getRawValues(
243 | targetEntity,
244 | targetEntity.name() + '_ListView',
245 | 'listView',
246 | 1,
247 | reference.perPage(),
248 | filterValues,
249 | filterFields,
250 | reference.getSortFieldName(),
251 | reference.sortDir()
252 | ));
253 |
254 | }
255 |
256 | return this.fillOptimizedReferencedData(calls, references);
257 | }
258 |
259 | /**
260 | * Fill all reference entries to return [{targetEntity.identifier: targetLabel}, ...]
261 | *
262 | * @param {[Promise]} apiCalls
263 | * @param {[Reference]} references
264 | * @returns {Promise}
265 | */
266 | fillOptimizedReferencedData(apiCalls, references) {
267 | return this._promisesResolver.allEvenFailed(apiCalls)
268 | .then((responses) => {
269 | if (responses.length === 0) {
270 | return {};
271 | }
272 |
273 | let referencedData = {},
274 | i = 0;
275 |
276 | for (let j in references) {
277 | let reference = references[j],
278 | response = responses[i++];
279 |
280 | // Retrieve entries depending on 1 or many request was done
281 | if (response.status == 'error') {
282 | // the response failed
283 | continue;
284 | }
285 |
286 | referencedData[reference.name()] = response.result.data;
287 | }
288 |
289 | return referencedData;
290 | });
291 | }
292 |
293 | /**
294 | * Fill all reference entries to return [{targetEntity.identifier: targetLabel}, ...]
295 | *
296 | * @param {[Promise]} apiCalls
297 | * @param {[Reference]} references
298 | * @param {[Object]} rawValues
299 | * @returns {Promise}
300 | */
301 | fillFilteredReferencedData(apiCalls, references, rawValues) {
302 | return this._promisesResolver.allEvenFailed(apiCalls)
303 | .then((responses) => {
304 | if (responses.length === 0) {
305 | return {};
306 | }
307 |
308 | let referencedData = {},
309 | response,
310 | i = 0;
311 |
312 | for (let j in references) {
313 | let data = [],
314 | reference = references[j],
315 | identifiers = reference.getIdentifierValues(rawValues);
316 |
317 | for (let k in identifiers) {
318 | response = responses[i++];
319 | if (response.status == 'error') {
320 | // one of the responses failed
321 | continue;
322 | }
323 | data.push(response.result);
324 | }
325 |
326 | if (!data.length) {
327 | continue;
328 | }
329 |
330 | referencedData[reference.name()] = data;
331 | }
332 |
333 | return referencedData;
334 | });
335 | }
336 |
337 | /**
338 | * Returns all ReferencedList for an entity for associated values [{targetEntity.identifier: [targetFields, ...]}}
339 | *
340 | * @param {View} referencedLists
341 | * @param {String} sortField
342 | * @param {String} sortDir
343 | * @param {*} entityId
344 | *
345 | * @returns {promise}
346 | */
347 | getReferencedListData(referencedLists, sortField, sortDir, entityId) {
348 | let getRawValues = this.getRawValues.bind(this),
349 | calls = [];
350 |
351 | for (let i in referencedLists) {
352 | let referencedList = referencedLists[i],
353 | targetEntity = referencedList.targetEntity(),
354 | viewName = referencedList.datagridName(),
355 | currentSortField = referencedList.getSortFieldName(),
356 | currentSortDir = referencedList.sortDir(),
357 | filter = {};
358 |
359 | if (sortField && sortField.split('.')[0] === viewName) {
360 | currentSortField = sortField;
361 | currentSortDir = sortDir || 'ASC';
362 | }
363 |
364 | const permanentFilters = referencedList.permanentFilters() || {};
365 | Object.keys(permanentFilters).forEach(key => {
366 | filter[key] = permanentFilters[key];
367 | });
368 | filter[referencedList.targetReferenceField()] = entityId;
369 |
370 | calls.push(getRawValues(targetEntity, viewName, 'listView', 1, referencedList.perPage(), filter, {}, currentSortField, currentSortDir));
371 | }
372 |
373 | return this._promisesResolver.allEvenFailed(calls)
374 | .then((responses) => {
375 | let j = 0,
376 | entries = {};
377 |
378 | for (let i in referencedLists) {
379 | let response = responses[j++];
380 | if (response.status == 'error') {
381 | // If a response fail, skip it
382 | continue;
383 | }
384 |
385 | entries[i] = response.result.data;
386 | }
387 |
388 | return entries;
389 | });
390 | }
391 |
392 | getRecordsByIds(entity, ids) {
393 | if (!ids || !ids.length) {
394 | return this._promisesResolver.empty();
395 | }
396 |
397 | let calls = ids.map(id => this.getOne(entity, 'listView', id, entity.identifier().name()));
398 |
399 | return this._promisesResolver.allEvenFailed(calls)
400 | .then(responses => responses.filter(r => r.status != 'error').map(r => r.result));
401 | }
402 | }
403 |
404 | export default ReadQueries;
405 |
--------------------------------------------------------------------------------
/tests/lib/View/ViewTest.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 |
3 | import Entity from "../../../lib/Entity/Entity";
4 | import Entry from "../../../lib/Entry";
5 | import Field from "../../../lib/Field/Field";
6 | import ReferenceField from "../../../lib/Field/ReferenceField";
7 | import ReferenceManyField from "../../../lib/Field/ReferenceManyField";
8 | import View from "../../../lib/View/View";
9 | import ListView from "../../../lib/View/ListView";
10 |
11 | describe('View', function() {
12 |
13 | it('should be disabled by default', () => {
14 | const view = new ListView().setEntity(new Entity('foobar'));
15 | assert.isFalse(view.enabled);
16 | });
17 |
18 | describe('name()', function() {
19 | it('should return a default name based on the entity name and view type', function() {
20 | var view = new ListView().setEntity(new Entity('foobar'));
21 | assert.equal(view.name(), 'foobar_ListView');
22 | });
23 | });
24 |
25 | describe('title()', function() {
26 | it('should return false by default', function () {
27 | var view = new ListView(new Entity('foobar'));
28 | assert.isFalse(view.title());
29 | });
30 |
31 | it('should return the view title', function () {
32 | var view = new View(new Entity('foobar')).title('my-title');
33 | assert.equal(view.title(), 'my-title');
34 | });
35 | });
36 |
37 | describe('description()', function() {
38 | it('should return empty string by default', function () {
39 | var view = new View(new Entity('foobar'));
40 | assert.equal(view.description(), '');
41 | });
42 |
43 | it('should return the view description', function () {
44 | var view = new View(new Entity('foobar')).description('my description');
45 | assert.equal(view.description(), 'my description');
46 | });
47 | });
48 |
49 | describe('getReferences()', function() {
50 | it('should return only reference and reference_many fields', function() {
51 | var post = new Entity('post');
52 | var category = new ReferenceField('category');
53 | var tags = new ReferenceManyField('tags');
54 | var view = new View(post).fields([
55 | new Field('title'),
56 | category,
57 | tags
58 | ]);
59 |
60 | assert.deepEqual({category: category, tags: tags}, view.getReferences());
61 | });
62 |
63 | it('should return only reference with remote complete if withRemoteComplete is true', function() {
64 | var post = new Entity('post');
65 | var category = new ReferenceField('category').remoteComplete(true);
66 | var tags = new ReferenceManyField('tags').remoteComplete(false);
67 | var view = new View(post).fields([
68 | new Field('title'),
69 | category,
70 | tags
71 | ]);
72 |
73 | assert.deepEqual({ category: category }, view.getReferences(true));
74 | });
75 |
76 | it('should return only reference with no remote complete if withRemoteComplete is false', function() {
77 | var post = new Entity('post');
78 | var category = new ReferenceField('category').remoteComplete(true);
79 | var tags = new ReferenceManyField('tags').remoteComplete(false);
80 | var view = new View(post).fields([
81 | new Field('title'),
82 | category,
83 | tags
84 | ]);
85 |
86 | assert.deepEqual({ tags: tags }, view.getReferences(false));
87 | });
88 | });
89 |
90 | describe('addField()', function() {
91 | it('should add fields and preserve the order', function () {
92 | var post = new Entity('post');
93 | var view = new View(post);
94 | var refMany = new ReferenceManyField('refMany');
95 | var ref = new ReferenceField('myRef');
96 |
97 | var field = new Field('body');
98 | view.addField(ref).addField(refMany).addField(field);
99 |
100 | assert.equal(view.getFieldsOfType('reference_many')[0].name(), 'refMany');
101 | assert.equal(view.getReferences()['myRef'].name(), 'myRef');
102 | assert.equal(view.getReferences()['refMany'].name(), 'refMany');
103 | assert.equal(view.getFields()[2].name(), 'body');
104 | });
105 | });
106 |
107 | describe('fields()', function() {
108 | it('should return the fields when called with no arguments', function() {
109 | var view = new View(new Entity('post'));
110 | var field = new Field('body');
111 | view.addField(field);
112 |
113 | assert.deepEqual(view.fields(), [field]);
114 | });
115 |
116 | it('should add fields when called with an array argument', function() {
117 | var view = new View(new Entity('post'));
118 | var field1 = new Field('foo');
119 | var field2 = new Field('bar');
120 | view.fields([field1, field2]);
121 |
122 | assert.deepEqual(view.fields(), [field1, field2]);
123 | });
124 |
125 | it('should keep the default order of the given array to equal to the index even when more than 10 fields', function() {
126 | var view = new View(new Entity('post'));
127 | var fields = Array.from(new Array(11).keys()).map(function (i) {
128 | return new Field(i);
129 | });
130 | view.fields(fields);
131 |
132 | assert.deepEqual(view.fields(), fields);
133 |
134 | fields.map(function (field, index) {
135 | assert.equal(field.order(), index);
136 | });
137 | });
138 |
139 | it('should add fields when called with a nested array argument', function() {
140 | var view = new View(new Entity('post'));
141 | var field1 = new Field('foo');
142 | var field2 = new Field('bar');
143 | view.fields([field1, [field2]]);
144 |
145 | assert.deepEqual(view.fields(), [field1, field2]);
146 | });
147 |
148 | it('should add a single field when called with a non array argument', function() {
149 | var view = new View(new Entity('post'));
150 | var field1 = new Field('foo');
151 | view.fields(field1);
152 |
153 | assert.deepEqual(view.fields(), [field1]);
154 | });
155 |
156 | it('should add fields when called with several arguments', function() {
157 | var view = new View(new Entity('post'));
158 | var field1 = new Field('foo');
159 | var field2 = new Field('bar');
160 | view.fields(field1, field2);
161 |
162 | assert.deepEqual(view.fields(), [field1, field2]);
163 | });
164 |
165 | it('should add field collections', function() {
166 | var view1 = new View(new Entity('post'));
167 | var view2 = new View(new Entity('category'));
168 | var field1 = new Field('foo');
169 | var field2 = new Field('bar');
170 | view1.fields(field1, field2);
171 | view2.fields(view1.fields());
172 |
173 | assert.deepEqual(view2.fields(), [field1, field2]);
174 | });
175 |
176 | it('should allow fields reuse', function() {
177 | var field1 = new Field('foo'), field2 = new Field('bar');
178 | var view1 = new View().addField(field1);
179 | var view2 = new View().fields([
180 | view1.fields(),
181 | field2
182 | ]);
183 |
184 | assert.deepEqual(view2.fields(), [field1, field2]);
185 | });
186 |
187 | it('should append fields when multiple calls', function() {
188 | var view = new View();
189 | var field1 = new Field('foo'), field2 = new Field('bar');
190 | view
191 | .fields(field1)
192 | .fields(field2);
193 |
194 | assert.deepEqual(view.fields(), [field1, field2]);
195 | });
196 |
197 | it('should enable the view when setting fields', () => {
198 | var view = new View();
199 |
200 | assert.isFalse(view.enabled);
201 |
202 | view.fields([new Field('test')]);
203 | assert.isTrue(view.enabled);
204 | });
205 | });
206 |
207 | describe('disable()', () => {
208 | it('should disable the view', () => {
209 | const view = new View();
210 | view.enable();
211 | assert.isTrue(view.enabled);
212 | view.disable();
213 | assert.isFalse(view.enabled);
214 | });
215 |
216 | it('should disable the view even if the fields have been set', () => {
217 | const view = new View();
218 |
219 | assert.isFalse(view.enabled);
220 | view.fields([new Field('test')]);
221 | assert.isTrue(view.enabled);
222 |
223 | view.disable();
224 | assert.isFalse(view.enabled);
225 | });
226 | });
227 |
228 | describe("validate()", function () {
229 | it('should call validator on each fields.', function () {
230 | var entry = new Entry(),
231 | view = new View('myView'),
232 | field1 = new Field('notValidable').label('Complex'),
233 | field2 = new Field('simple').label('Simple');
234 |
235 | entry.values = {
236 | notValidable: false,
237 | simple: 1
238 | };
239 |
240 | view.addField(field1).addField(field2);
241 |
242 | field1.validation().validator = function () {
243 | throw new Error('Field "Complex" is not valid.');
244 | };
245 | field2.validation().validator = function () {
246 | return true;
247 | };
248 |
249 | assert.throw(function () { view.validate(entry); }, Error, 'Field "Complex" is not valid.');
250 | });
251 |
252 | it('should call validator with the targeted field as first parameter', function (done) {
253 | var entry = new Entry(),
254 | view = new View('myView'),
255 | field1 = new Field('field1').label('field1');
256 |
257 | entry.values = {
258 | field1: "field1_value",
259 | };
260 |
261 | view.addField(field1);
262 |
263 | field1.validation().validator = function (value) {
264 | assert.equal("field1_value", value);
265 | done();
266 | };
267 |
268 | view.validate(entry);
269 | });
270 |
271 | it('should call validator with the all other fields as second parameter', function (done) {
272 | var entry = new Entry(),
273 | view = new View('myView'),
274 | field1 = new Field('field1').label('field1'),
275 | field2 = new Field('field2').label('field2');
276 |
277 | entry.values = {
278 | field1: "field1_value",
279 | field2: "field2_value",
280 | };
281 |
282 | view.addField(field1).addField(field2);
283 |
284 | field1.validation().validator = function (value, all) {
285 | assert.deepEqual({
286 | field1: "field1_value",
287 | field2: "field2_value",
288 | }, all);
289 | done();
290 | };
291 |
292 | view.validate(entry);
293 | });
294 | });
295 |
296 | describe('mapEntry()', () => {
297 |
298 | it('should return an entry', () => {
299 | let view = new View();
300 | view.setEntity(new Entity().name('Foo'));
301 | assert.instanceOf(view.mapEntry(), Entry);
302 | assert.equal(view.mapEntry().entityName, 'Foo');
303 | });
304 |
305 | it('should return an empty entry when passed an empty object', () => {
306 | let view = new View();
307 | view.setEntity(new Entity().name('Foo'));
308 | assert.deepEqual(view.mapEntry({}).values, {});
309 | });
310 |
311 | it('should use default values when passed an empty object', () => {
312 | let view = new View();
313 | view.setEntity(new Entity().name('Foo'));
314 | view.fields([
315 | new Field('foo').defaultValue('bar')
316 | ]);
317 | assert.equal(view.mapEntry({}).values.foo, 'bar');
318 | });
319 |
320 | it('should not use default values when passed a non-empty object', () => {
321 | let view = new View();
322 | view.setEntity(new Entity().name('Foo'));
323 | view.fields([
324 | new Field('foo').defaultValue('bar')
325 | ]);
326 | assert.notEqual(view.mapEntry({ hello: 1 }).values.foo, 'bar');
327 | });
328 |
329 | it('should populate the entry based on the values passed as argument', () => {
330 | let view = new View();
331 | view.setEntity(new Entity().name('Foo'));
332 | let entry = view.mapEntry({ hello: 1, world: 2 });
333 | assert.equal(entry.values.hello, 1);
334 | assert.equal(entry.values.world, 2);
335 | });
336 |
337 | it('should set the entry identifier value by default', () => {
338 | let view = new View();
339 | view.setEntity(new Entity().name('Foo'));
340 | assert.equal(view.mapEntry({ id: 1, bar: 2 }).identifierValue, 1);
341 | });
342 |
343 | it('should set the entry identifier value according to the fields', () => {
344 | let view = new View();
345 | view.setEntity(new Entity().name('Foo').identifier(new Field('bar')));
346 | assert.equal(view.mapEntry({ id: 1, bar: 2 }).identifierValue, 2);
347 | });
348 |
349 | it('should transform the object values using the fields map functions', () => {
350 | let view = new View();
351 | view.setEntity(new Entity().name('Foo'));
352 | view.fields([
353 | new Field('foo').map(v => v - 1)
354 | ]);
355 | assert.equal(view.mapEntry({ foo: 2 }).values.foo, 1);
356 | })
357 | });
358 |
359 | describe('mapEntries()', () => {
360 | it('should return entries based on an array of objects', function () {
361 | let view = new View();
362 | view
363 | .addField(new Field('title'))
364 | .setEntity(new Entity().identifier(new Field('post_id')));
365 |
366 | let entries = view.mapEntries([
367 | { post_id: 1, title: 'Hello', published: true},
368 | { post_id: 2, title: 'World', published: false},
369 | { post_id: 3, title: 'How to use ng-admin', published: false}
370 | ]);
371 |
372 | assert.equal(entries.length, 3);
373 | assert.equal(entries[0].identifierValue, 1);
374 | assert.equal(entries[1].values.title, 'World');
375 | assert.equal(entries[1].values.published, false);
376 | });
377 | });
378 |
379 | describe('transformEntry()', () => {
380 |
381 | it('should return an empty object for empty entries', () => {
382 | let view = new View();
383 | let entry = new Entry();
384 | assert.deepEqual(view.transformEntry(entry), {});
385 | });
386 |
387 | it('should return an object litteral based on the entry values', () => {
388 | let view = new View();
389 | let entry = new Entry('foo', { id: 1, bar: 2 }, 1);
390 | assert.deepEqual(view.transformEntry(entry), { id: 1, bar: 2 });
391 | });
392 |
393 | it('should transform the entry values using the fields transform functions', () => {
394 | let view = new View();
395 | view.setEntity(new Entity().name('Foo'));
396 | view.fields([
397 | new Field('bar').transform(v => v - 1)
398 | ]);
399 | let entry = new Entry('foo', { id: 1, bar: 2 }, 1);
400 | assert.deepEqual(view.transformEntry(entry), { id: 1, bar: 1 });
401 |
402 | });
403 | });
404 |
405 | describe('identifier()', function() {
406 | it('should return the identifier.', function () {
407 | var entity = new Entity('post').identifier(new Field('post_id'));
408 | var view = entity.listView();
409 | view.addField(new Field('name'));
410 |
411 | assert.equal(view.identifier().name(), 'post_id');
412 | });
413 | });
414 | });
415 |
--------------------------------------------------------------------------------