Skip to content

Commit

Permalink
Merge pull request #714 from marmelab/embedded_list
Browse files Browse the repository at this point in the history
[RFR] Introducing the embedded_list field type
  • Loading branch information
fzaninotto committed Oct 20, 2015
2 parents c16e739 + 844eb22 commit 0d95179
Show file tree
Hide file tree
Showing 23 changed files with 775 additions and 73 deletions.
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 @@ -153,6 +157,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 @@ -171,7 +183,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

0 comments on commit 0d95179

Please sign in to comment.