Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[RFR] Introducing the embedded_list field type #714

Merged
merged 12 commits into from
Oct 20, 2015
4 changes: 2 additions & 2 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"esnext": true,
"bitwise": true,
"camelcase": true,
"curly": true,
"curly": false,
"eqeqeq": true,
"immed": true,
"indent": 2,
Expand All @@ -14,7 +14,7 @@
"regexp": true,
"undef": true,
"unused": true,
"strict": true,
"strict": false,
"trailing": true,
"smarttabs": true,
"globals": {
Expand Down
361 changes: 323 additions & 38 deletions doc/Configuration-reference.md

Large diffs are not rendered by default.

46 changes: 45 additions & 1 deletion examples/blog/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
.cssClasses('hidden-xs'),
nga.field('views', 'number')
.cssClasses('hidden-xs'),
nga.field('backlinks', 'embedded_list') // display list of related comments
.label('Links')
.map(links => links ? links.length : '')
.template('{{ value }}'),
nga.field('tags', 'reference_many') // a Reference is a particular type of field that references another entity
.targetEntity(tag) // the tag entity is defined later in this file
.targetField(nga.field('name')) // the field to be displayed in this list
Expand Down Expand Up @@ -152,6 +156,14 @@
.cssClasses('col-sm-4'),
nga.field('average_note', 'float')
.cssClasses('col-sm-4'),
nga.field('backlinks', 'embedded_list') // display embedded list
.targetFields([
nga.field('date', 'datetime'),
nga.field('url')
.cssClasses('col-lg-10')
])
.sortField('date')
.sortDir('DESC'),
nga.field('comments', 'referenced_list') // display list of related comments
.targetEntity(nga.entity('comments'))
.targetReferenceField('post_id')
Expand All @@ -170,7 +182,39 @@
post.showView() // a showView displays one entry in full page - allows to display more data than in a a list
.fields([
nga.field('id'),
post.editionView().fields(), // reuse fields from another view in another order
nga.field('category', 'choice') // a choice field is rendered as a dropdown in the edition view
.choices([ // List the choice as object literals
{ label: 'Tech', value: 'tech' },
{ label: 'Lifestyle', value: 'lifestyle' }
]),
nga.field('subcategory', 'choice')
.choices(subCategories),
nga.field('tags', 'reference_many') // ReferenceMany translates to a select multiple
.targetEntity(tag)
.targetField(nga.field('name')),
nga.field('pictures', 'json'),
nga.field('views', 'number'),
nga.field('average_note', 'float'),
nga.field('backlinks', 'embedded_list') // display embedded list
.targetFields([
nga.field('date', 'datetime'),
nga.field('url')
])
.sortField('date')
.sortDir('DESC'),
nga.field('comments', 'referenced_list') // display list of related comments
.targetEntity(nga.entity('comments'))
.targetReferenceField('post_id')
.targetFields([
nga.field('id').isDetailLink(true),
nga.field('created_at').label('Posted'),
nga.field('body').label('Comment')
])
.sortField('created_at')
.sortDir('DESC')
.listActions(['edit']),
nga.field('').label('')
.template('<span class="pull-right"><ma-filtered-list-button entity-name="comments" filter="{ post_id: entry.values.id }" size="sm"></ma-filtered-list-button><ma-create-button entity-name="comments" size="sm" label="Create related comment" default-values="{ post_id: entry.values.id }"></ma-create-button></span>'),
nga.field('custom_action').label('')
.template('<send-email post="entry"></send-email>')
]);
Expand Down
36 changes: 22 additions & 14 deletions examples/blog/data.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions examples/blog/fakerest-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
var restServer = new FakeRest.Server('http://localhost:3000');
var testEnv = window.location.pathname.indexOf('test.html') !== -1;
restServer.init(apiData);
restServer.setDefaultQuery(function(resourceName) {
if (resourceName == 'posts') return { embed: ['comments'] }
return {};
});
restServer.toggleLogging(); // logging is off by default, enable it

// use sinon.js to monkey-patch XmlHttpRequest
Expand Down
5 changes: 5 additions & 0 deletions examples/blog/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
<title>Angular admin</title>
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="http://localhost:8000/build/ng-admin.min.css">
<style type="text/css">
.ng-admin-entity-posts .ng-admin-column-title {
max-width: 250px;
}
</style>
</head>
<body ng-app="myApp" ng-strict-di>
<div ui-view></div>
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"url": "git://github.com/marmelab/ng-admin.git"
},
"devDependencies": {
"admin-config": "^0.4.0",
"admin-config": "^0.5.1",
"angular": "~1.3.15",
"angular-bootstrap": "^0.12.0",
"angular-mocks": "1.3.14",
Expand All @@ -29,7 +29,7 @@
"es6-promise": "^2.3.0",
"exports-loader": "^0.6.2",
"extract-text-webpack-plugin": "^0.8.0",
"fakerest": "^1.0.10",
"fakerest": "^1.1.4",
"file-loader": "^0.8.1",
"font-awesome": "^4.3.0",
"grunt": "~0.4.4",
Expand Down
2 changes: 2 additions & 0 deletions src/javascripts/ng-admin/Crud/CrudModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ CrudModule.directive('maButtonField', require('./field/maButtonField'));
CrudModule.directive('maChoiceField', require('./field/maChoiceField'));
CrudModule.directive('maChoicesField', require('./field/maChoicesField'));
CrudModule.directive('maDateField', require('./field/maDateField'));
CrudModule.directive('maEmbeddedListField', require('./field/maEmbeddedListField'));
CrudModule.directive('maInputField', require('./field/maInputField'));
CrudModule.directive('maJsonField', require('./field/maJsonField'));
CrudModule.directive('maFileField', require('./field/maFileField'));
Expand Down Expand Up @@ -54,6 +55,7 @@ CrudModule.directive('maColumn', require('./column/maColumn'));
CrudModule.directive('maBooleanColumn', require('./column/maBooleanColumn'));
CrudModule.directive('maChoicesColumn', require('./column/maChoicesColumn'));
CrudModule.directive('maDateColumn', require('./column/maDateColumn'));
CrudModule.directive('maEmbeddedListColumn', require('./column/maEmbeddedListColumn'));
CrudModule.directive('maJsonColumn', require('./column/maJsonColumn'));
CrudModule.directive('maNumberColumn', require('./column/maNumberColumn'));
CrudModule.directive('maReferenceColumn', require('./column/maReferenceColumn'));
Expand Down
85 changes: 85 additions & 0 deletions src/javascripts/ng-admin/Crud/column/maEmbeddedListColumn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Entry from 'admin-config/lib/Entry';

function sorter(sortField, sortDir) {
return (entry1, entry2) => {
// use < and > instead of substraction to sort strings properly
const sortFactor = sortDir === 'DESC' ? -1 : 1;
if (entry1.values[sortField] > entry2.values[sortField]) return sortFactor;
if (entry1.values[sortField] < entry2.values[sortField]) return -1 * sortFactor;
return 0;
};
}

function maEmbeddedListColumn(NgAdminConfiguration) {
const application = NgAdminConfiguration(); // jshint ignore:line
return {
scope: {
'field': '&',
'value': '&',
'datastore': '&'
},
restrict: 'E',
link: {
pre: function(scope) {
const field = scope.field();
const targetEntity = field.targetEntity();
const targetEntityName = targetEntity.name();
const targetFields = field.targetFields();
const sortField = field.sortField();
const sortDir = field.sortDir();
var filterFunc;
if (field.permanentFilters()) {
const filters = field.permanentFilters();
const filterKeys = Object.keys(filters);
filterFunc = (entry) => filterKeys.reduce((isFiltered, key) => isFiltered && entry.values[key] === filters[key], true);
} else {
filterFunc = () => true;
}
let entries = Entry
.createArrayFromRest(scope.value() || [], targetFields, targetEntityName, targetEntity.identifier().name())
.sort(sorter(sortField, sortDir))
.filter(filterFunc);
if (!targetEntityName) {
let index = 0;
entries = entries.map(e => {
e._identifierValue = index++;
return e;
});
}
scope.field = field;
scope.targetFields = targetFields;
scope.entries = entries;
scope.entity = targetEntityName ? application.getEntity(targetEntityName) : targetEntity;
scope.sortField = sortField;
scope.sortDir = sortDir;
scope.sort = field => {
let sortDir = 'ASC';
const sortField = field.name();
if (scope.sortField === sortField) {
// inverse sort dir
sortDir = scope.sortDir === 'ASC' ? 'DESC' : 'ASC';
}
scope.entries = scope.entries.sort(sorter(sortField, sortDir));
scope.sortField = sortField;
scope.sortDir = sortDir;
};
}
},
template: `
<ma-datagrid ng-if="::entries.length > 0"
entries="entries"
fields="::targetFields"
list-actions="::field.listActions()"
entity="::entity"
datastore="::datastore()"
sort-field="{{ sortField }}"
sort-dir="{{ sortDir }}"
sort="::sort">
</ma-datagrid>`
};
}

maEmbeddedListColumn.$inject = ['NgAdminConfiguration'];

module.exports = maEmbeddedListColumn;

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function maReferencedListColumn(NgAdminConfiguration) {
}
},
template: `
<ma-datagrid name="{{ field.datagridName() }}"
<ma-datagrid ng-if="::entries.length > 0" name="{{ field.datagridName() }}"
entries="::entries"
fields="::field.targetFields()"
list-actions="::field.listActions()"
Expand Down
1 change: 1 addition & 0 deletions src/javascripts/ng-admin/Crud/config/factories.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ function factories(fvp) {
fvp.registerFieldView('date', require('../fieldView/DateFieldView'));
fvp.registerFieldView('datetime', require('../fieldView/DateTimeFieldView'));
fvp.registerFieldView('email', require('../fieldView/EmailFieldView'));
fvp.registerFieldView('embedded_list', require('../fieldView/EmbeddedListFieldView'));
fvp.registerFieldView('file', require('../fieldView/FileFieldView'));
fvp.registerFieldView('float', require('../fieldView/FloatFieldView'));
fvp.registerFieldView('json', require('../fieldView/JsonFieldView'));
Expand Down
71 changes: 71 additions & 0 deletions src/javascripts/ng-admin/Crud/field/maEmbeddedListField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Entry from 'admin-config/lib/Entry';

function maEmbeddedListField() {
return {
scope: {
'field': '&',
'value': '=',
'datastore': '&'
},
restrict: 'E',
link: {
pre: function(scope) {
const field = scope.field();
const targetEntity = field.targetEntity();
const targetEntityName = targetEntity.name();
const targetFields = field.targetFields();
const sortField = field.sortField();
const sortDir = field.sortDir() === 'DESC' ? -1 : 1;
var filterFunc;
if (field.permanentFilters()) {
const filters = field.permanentFilters();
const filterKeys = Object.keys(filters);
filterFunc = (entry) => {
return filterKeys.reduce((isFiltered, key) => isFiltered && entry.values[key] === filters[key], true);
};
} else {
filterFunc = () => true;
}
scope.fields = targetFields;
scope.entries = Entry
.createArrayFromRest(scope.value || [], targetFields, targetEntityName, targetEntity.identifier().name())
.sort((entry1, entry2) => {
// use < and > instead of substraction to sort strings properly
if (entry1.values[sortField] > entry2.values[sortField]) return sortDir;
if (entry1.values[sortField] < entry2.values[sortField]) return -1 * sortDir;
return 0;
})
.filter(filterFunc);
scope.addNew = () => scope.entries.push(Entry.createForFields(targetFields));
scope.remove = entry => {
scope.entries = scope.entries.filter(e => e !== entry);
};
scope.$watch('entries', (newEntries, oldEntries) => {
if (newEntries === oldEntries) return;
scope.value = newEntries.map(e => e.transformToRest(targetFields));
}, true);
}
},
template: `
<div class="row"><div class="col-sm-12">
<ng-form ng-repeat="entry in entries track by $index" class="subentry" name="subform_{{$index}}" ng-init="formName = 'subform_' + $index">
<div class="remove_button_container">
<a class="btn btn-default btn-sm" ng-click="remove(entry)"><span class="glyphicon glyphicon-minus-sign" aria-hidden="true"></span>&nbsp;Remove</a>
</div>
<div class="form-field form-group" ng-repeat="field in ::fields track by $index">
<ma-field field="::field" value="entry.values[field.name()]" entry="entry" entity="::entity" form="formName" datastore="::datastore()"></ma-field>
</div>
<hr/>
</ng-form>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<a class="btn btn-default btn-sm" ng-click="addNew()"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>&nbsp;Add new {{ field().name() }}</a>
</div>
</div>
</div></div>`
};
}

maEmbeddedListField.$inject = [];

module.exports = maEmbeddedListField;
2 changes: 1 addition & 1 deletion src/javascripts/ng-admin/Crud/fieldView/DateFieldView.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ module.exports = {
getReadWidget: () => '<ma-date-column field="::field" value="::value"></ma-date-column>',
getLinkWidget: () => '<a ng-click="gotoDetail()">' + module.exports.getReadWidget() + '</a>',
getFilterWidget: () => '<ma-date-field field="::field" value="value"></ma-date-field>',
getWriteWidget: () => '<div class="row"><div class="col-sm-5 col-lg-4"><ma-date-field field="::field" value="value"></ma-date-field></div></div>'
getWriteWidget: () => '<div class="date_widget"><ma-date-field field="::field" value="value"></ma-date-field></div>'
};
Loading