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] Revamp Filter UX #522

Merged
merged 9 commits into from
Jun 30, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,19 @@ Add filters to the list. Each field maps a property in the API endpoint result.
nga.field('age', 'number')
]);

Filters appear when the user clicks on the "Add filter" button at the top of the list. You can also set a filter field as "pinned", to be sure it's always displayed.

listView.filters([
nga.field('q').label('Search').pinned(true)
]);

Filter fields can be of any type, including `reference` and `template`. this allows to define custom filters with ease.

listView.filters([
nga.field('q', 'template').label('')
.template('<div class="input-group"><input type="text" ng-model="value" placeholder="Search" class="form-control"></input><span class="input-group-addon"><i class="glyphicon glyphicon-search"></i></span></div>'),
]);

* `listActions(String|Array)`
Add an action column with action buttons on each line. You can pass a list of button names among 'show', 'edit', and 'delete'.

Expand Down Expand Up @@ -455,6 +468,9 @@ A list of CSS classes to be added to the corresponding field. If you provide a f
* `defaultValue(*)`
Define the default value of the field in the creation form.

* `pinned(boolean)`
Whether the field should always appear. Used in filters (see listView Settings). Default to false.

### `number` Field Settings

* `format(string)`
Expand Down
16 changes: 4 additions & 12 deletions examples/blog/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,13 @@
.targetField(nga.field('title').map(truncate))
])
.filters([
nga.field('q', 'string').label('').attributes({'placeholder': 'Global Search'}),
nga.field('q', 'template')
.label('')
.pinned(true)
.template('<div class="input-group"><input type="text" ng-model="value" placeholder="Search" class="form-control"></input><span class="input-group-addon"><i class="glyphicon glyphicon-search"></i></span></div>'),
nga.field('created_at', 'date')
.label('Posted')
.attributes({'placeholder': 'Filter by date'}),
nga.field('today', 'boolean').map(function() {
var now = new Date(),
year = now.getFullYear(),
month = now.getMonth() + 1,
day = now.getDate();
month = month < 10 ? '0' + month : month;
day = day < 10 ? '0' + day : day;
return {
created_at: [year, month, day].join('-') // ?created_at=... will be appended to the API call
};
}),
nga.field('post_id', 'reference')
.label('Post')
.targetEntity(post)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"jsonlint": "^1.6.2",
"karma": "~0.12.14",
"karma-babel-preprocessor": "^4.0.0",
"karma-chrome-launcher": "^0.1.4",
"karma-chrome-launcher": "^0.2.0",
"karma-jasmine": "~0.3.3",
"karma-ng-html2js-preprocessor": "~0.1.0",
"karma-ng-scenario": "~0.1.0",
Expand Down
6 changes: 4 additions & 2 deletions src/javascripts/ng-admin/Crud/CrudModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var CrudModule = angular.module('crud', [
'ui.router', 'ui.bootstrap', 'ngSanitize', 'textAngular', 'ngInflection', 'ui.codemirror', 'ngFileUpload', 'ngNumeraljs'
]);

CrudModule.controller('ListLayoutController', require('./list/ListLayoutController'));
CrudModule.controller('ListController', require('./list/ListController'));
CrudModule.controller('ShowController', require('./show/ShowController'));
CrudModule.controller('FormController', require('./form/FormController'));
Expand Down Expand Up @@ -39,13 +40,14 @@ CrudModule.directive('uiSelectRequired', require('./field/uiSelectRequired'));

CrudModule.provider('FieldViewConfiguration', require('./fieldView/FieldViewConfiguration'));

CrudModule.directive('listActions', require('./list/ListActions'));
CrudModule.directive('maListActions', require('./list/maListActions'));
CrudModule.directive('maDatagrid', require('./list/maDatagrid'));
CrudModule.directive('maDatagridPagination', require('./list/maDatagridPagination'));
CrudModule.directive('maDatagridInfinitePagination', require('./list/maDatagridInfinitePagination'));
CrudModule.directive('maDatagridItemSelector', require('./list/maDatagridItemSelector'));
CrudModule.directive('maDatagridMultiSelector', require('./list/maDatagridMultiSelector'));
CrudModule.directive('maFilter', require('./filter/maFilter'));
CrudModule.directive('maFilterButton', require('./filter/maFilterButton'));

CrudModule.directive('maColumn', require('./column/maColumn'));
CrudModule.directive('maBooleanColumn', require('./column/maBooleanColumn'));
Expand All @@ -68,9 +70,9 @@ CrudModule.directive('maListButton', require('./button/maListButton'));
CrudModule.directive('maDeleteButton', require('./button/maDeleteButton'));
CrudModule.directive('maBatchDeleteButton', require('./button/maBatchDeleteButton'));
CrudModule.directive('maExportToCsvButton', require('./button/maExportToCsvButton'));
CrudModule.directive('maViewBatchActions', require('./button/maViewBatchActions'));

CrudModule.directive('maViewActions', require('./misc/ViewActions'));
CrudModule.directive('maViewBatchActions', require('./misc/ViewBatchActions'));
CrudModule.directive('compile', require('./misc/Compile'));

CrudModule.config(require('./routing'));
Expand Down
47 changes: 47 additions & 0 deletions src/javascripts/ng-admin/Crud/button/maViewBatchActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
function maViewBatchActionsDirective($injector) {
var $compile = $injector.get('$compile');

return {
restrict: 'E',
scope: {
'entity': '=',
'selection': '=',
'buttons': '&'
},
link: function(scope) {
scope.isopen = false;

scope.toggleDropdown = function($event) {
$event.preventDefault();
$event.stopPropagation();
scope.isopen = !scope.isopen;
};

scope.buttons = scope.buttons();
if (typeof scope.buttons === 'string') {
scope.customTemplate = scope.buttons;
scope.buttons = null;
}
},
template:
`<span ng-if="selection" class="btn-group" dropdown is-open="isopen">
<button type="button" ng-if="selection.length" class="btn btn-default dropdown-toggle" dropdown-toggle >
{{ selection.length }} Selected <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="button in buttons" ng-switch="button">
<a ng-switch-when="delete">
<ma-batch-delete-button selection="selection" entity="entity"/>
</a>
<a ng-switch-default>
<span compile="button"></span>
</a>
</li>
</ul>
</span>`
};
}

maViewBatchActionsDirective.$inject = ['$injector'];

module.exports = maViewBatchActionsDirective;
5 changes: 3 additions & 2 deletions src/javascripts/ng-admin/Crud/field/maInputField.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ define(function (require) {
}
},
template:
'<input type="{{ type || text }}" ng-attr-step="{{ step }}" ng-model="value" id="{{ name }}" name="{{ name }}" class="form-control"' +
'ng-required="v.required" ng-minlength="v.minlength" ng-maxlength="v.maxlength" />'
`<input type="{{ type || 'text' }}" ng-attr-step="{{ step }}" ng-model="value"
id="{{ name }}" name="{{ name }}" class="form-control"
'ng-required="v.required" ng-minlength="v.minlength" ng-maxlength="v.maxlength" />`
};
}

Expand Down
3 changes: 2 additions & 1 deletion src/javascripts/ng-admin/Crud/field/maTemplateField.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ define(function (require) {
scope: {
field: '&',
entry: '&',
entity: '&'
entity: '&',
value: '='
},
link: function(scope) {
scope.field = scope.field();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ define(function(require) {
return '<a ng-click="gotoDetail()">' + getReadWidget() + '</a>';
}
function getFilterWidget() {
return '<ma-button-field field="::field" value="values[field.name()]"></ma-button-field>';
return '<ma-checkbox-field field="::field" value="values[field.name()]"></ma-checkbox-field>';
}
function getWriteWidget() {
return '<ma-checkbox-field field="::field" value="entry.values[field.name()]"></ma-checkbox-field>';
Expand Down
34 changes: 17 additions & 17 deletions src/javascripts/ng-admin/Crud/filter/maFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,30 @@ function maFilterDirective(FieldViewConfiguration) {
}).join('');

var template = `
<form class="filters navbar-form well well-sm" ng-if="filterCtrl.shouldFilter()" ng-submit="filterCtrl.filter()">
<div class="filter form-group input-{{ field.type() }}" ng-repeat="field in filters track by $index" ng-class="{\'input-group\':field.label()}">
<label for="{{ field.name() }}" ng-if="field.label() && field.type() != \'boolean\'" class="input-group-addon">
{{ field.label() }}<span ng-if="field.validation().required">&nbsp;*</span>&nbsp;
</label>
<div ng-switch="field.type()" ng-class="field.getCssClasses(entry)">
${filterWidgetTypes}
</div>
<div class="row">
<form class="filters col-md-offset-6 col-md-6 form-horizontal" ng-if="filterCtrl.shouldFilter()">
<div class="filter form-group input-{{ field.type() }}" ng-repeat="field in filters track by $index">
<div class="col-sm-1 col-xs-1 remove_filter">
<a ng-if="!field.pinned()" ng-click="filterCtrl.removeFilter(field)"><span class="glyphicon glyphicon-remove"></span></a>
</div>
<button class="btn btn-default" type="submit">
<span class="glyphicon glyphicon-search"></span> Filter
</button>
<button ng-if="!filterCtrl.isFilterEmpty" class="btn btn-default" type="button" ng-click="filterCtrl.clearFilters()">
<span class="glyphicon glyphicon-remove"></span> Clear
</button>
</form>
<label for="{{ field.name() }}" class="col-sm-4 col-xs-11 control-label">
{{ field.label() }}<span ng-if="field.validation().required">&nbsp;*</span>&nbsp;
</label>
<div class="col-sm-7" ng-switch="field.type()" ng-class="field.getCssClasses(entry)">
${filterWidgetTypes}
</div>
</div>
</form>
</div>
`;

return {
restrict: 'E',
template: template,
scope: {
filters: '&',
datastore: '&'
filters: '=',
datastore: '&',
values: '&'
},
controllerAs: 'filterCtrl',
controller: FilterController
Expand Down
33 changes: 33 additions & 0 deletions src/javascripts/ng-admin/Crud/filter/maFilterButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
function maFilterButtonDirective() {
'use strict';

return {
restrict: 'E',
scope: {
filters: '&',
enabledFilters: '=',
enableFilter: '&'
},
link: function(scope) {
scope.notYetEnabledFilters = () => scope.filters().filter(filter =>
scope.enabledFilters.indexOf(filter) === -1
);
scope.hasFilters = () => scope.notYetEnabledFilters().length > 0;
},
template:
`<span class="btn-group" dropdown is-open="isopen" ng-if="hasFilters()">
<button type="button" class="btn btn-default dropdown-toggle" dropdown-toggle >
<span class="glyphicon glyphicon-filter" aria-hidden="true"></span>&nbsp;Add filter <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="filter in notYetEnabledFilters()" ng-switch="button">
<a ng-click="enableFilter()(filter)">{{ filter.label() }}</a>
</li>
</ul>
</span>`
};
}

maFilterButtonDirective.$inject = [];

module.exports = maFilterButtonDirective;
33 changes: 20 additions & 13 deletions src/javascripts/ng-admin/Crud/filter/maFilterController.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ function maFilterController($scope, $state, $stateParams) {
this.$scope = $scope;
this.$state = $state;
this.$stateParams = $stateParams;
this.$scope.values = this.$stateParams.search || {};
this.$scope.filters = this.$scope.filters();
this.$scope.values = this.$scope.values() || {};
$scope.$watch('values', (newValues, oldValues) => {
if (newValues != oldValues) {
this.filter(); // FIXME use debounce
Copy link
Contributor

Choose a reason for hiding this comment

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

FIXME spotted! :)

Copy link
Member Author

Choose a reason for hiding this comment

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

I know, will do in a future PR. Call me technical debt.

}
}, true)
this.$scope.filters = this.$scope.filters;
this.$scope.datastore = this.$scope.datastore();
this.isFilterEmpty = isEmpty(this.$scope.values);
}
Expand All @@ -26,6 +31,14 @@ function isEmpty(values) {
return true;
}

maFilterController.prototype.removeFilter = function(filter) {
delete this.$scope.values[filter.name()];
this.$scope.filters = this.$scope.filters.filter(f => f !== filter);
if (filter.name() in this.$stateParams.search) {
this.filter();
}
};

maFilterController.prototype.filter = function () {
var values = {},
filters = this.$scope.filters,
Expand All @@ -36,6 +49,10 @@ maFilterController.prototype.filter = function () {
for (i in filters) {
field = filters[i];
fieldName = field.name();
if (this.$scope.values[fieldName] === '') {
delete this.$scope.values[fieldName];
continue;
}

if ((field.type() === 'boolean' && this.$scope.values[fieldName]) || // for boolean false is the same as null
(field.type() !== 'boolean' && this.$scope.values[fieldName] !== null)) {
Expand All @@ -45,23 +62,13 @@ maFilterController.prototype.filter = function () {

this.$stateParams.search = values;
this.$stateParams.page = 1;
this.$state.go(this.$state.current, this.$stateParams, { reload: true, inherit: false, notify: true });
this.$state.go('list', this.$stateParams);
};

maFilterController.prototype.shouldFilter = function () {
return Object.keys(this.$scope.filters).length;
};

maFilterController.prototype.clearFilters = function () {
var i;

for (i in this.$scope.values) {
this.$scope.values[i] = null;
}

this.filter();
};

maFilterController.prototype.destroy = function () {
this.$scope = undefined;
};
Expand Down
33 changes: 0 additions & 33 deletions src/javascripts/ng-admin/Crud/list/Datagrid.html

This file was deleted.

8 changes: 0 additions & 8 deletions src/javascripts/ng-admin/Crud/list/ListActions.html

This file was deleted.

Loading