From 92b860798648e7b425b70e210fd4aa429eafbdf9 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Mon, 15 Jun 2015 23:29:09 +0200 Subject: [PATCH 01/10] Introduce new way to define dashboard datasources --- examples/blog/config.js | 68 ++++++++------- .../controller/DashboardController.js | 37 +++----- .../component/directive/maDashboardPanel.js | 3 +- .../provider/NgAdminConfiguration.js | 8 ++ .../Main/component/service/PanelBuilder.js | 87 +++++++++---------- .../ng-admin/Main/config/routing.js | 4 +- .../ng-admin/Main/view/dashboard.html | 34 ++++---- 7 files changed, 120 insertions(+), 121 deletions(-) diff --git a/examples/blog/config.js b/examples/blog/config.js index cd13d07b..9d793ef1 100644 --- a/examples/blog/config.js +++ b/examples/blog/config.js @@ -62,14 +62,6 @@ .addEntity(comment); // customize entities and views - - post.dashboardView() // customize the dashboard panel for this entity - .name('posts') - .title('Recent posts') - .order(1) // display the post panel first in the dashboard - .perPage(5) // limit the panel to the 5 latest posts - .fields([nga.field('title').isDetailLink(true).map(truncate)]); // fields() called with arguments add fields to the view - post.listView() .title('All posts') // default title is "[Entity_name] list" .description('List of posts with infinite pagination') // description appears under the title @@ -150,20 +142,6 @@ .template('') ]); - comment.dashboardView() - .title('Last comments') - .order(2) // display the comment panel second in the dashboard - .perPage(5) - .fields([ - nga.field('id'), - nga.field('body', 'wysiwyg') - .label('Comment') - .stripTags(true) - .map(truncate), - nga.field(null, 'template') // template fields don't need a name in dashboard view - .label('') - .template('') // you can use custom directives, too - ]); comment.listView() .title('Comments') @@ -229,15 +207,6 @@ comment.deletionView() .title('Deletion confirmation'); // customize the deletion confirmation message - tag.dashboardView() - .title('Recent tags') - .order(3) - .perPage(10) - .fields([ - nga.field('id'), - nga.field('name'), - nga.field('published', 'boolean').label('Is published ?') - ]); tag.listView() .infinitePagination(false) // by default, the list view uses infinite pagination. Set to false to use regulat pagination @@ -283,6 +252,43 @@ ) ); + // customize dashboard + admin.dashboard(nga.dashboard() + .addCollection('posts', nga.collection(post) + .title('Recent posts') + .perPage(5) // limit the panel to the 5 latest posts + .fields([ + nga.field('title').isDetailLink(true).map(truncate) + ]) + .order(1) + ) + .addCollection('comments', nga.collection(comment) + .title('Last comments') + .perPage(5) + .fields([ + nga.field('id'), + nga.field('body', 'wysiwyg') + .label('Comment') + .stripTags(true) + .map(truncate), + nga.field(null, 'template') // template fields don't need a name in dashboard view + .label('') + .template('') // you can use custom directives, too + ]) + .order(2) + ) + .addCollection('tags', nga.collection(tag) + .title('Recent tags') + .perPage(10) + .fields([ + nga.field('id'), + nga.field('name'), + nga.field('published', 'boolean').label('Is published ?') + ]) + .order(3) + ) + ); + nga.configure(admin); }]); diff --git a/src/javascripts/ng-admin/Main/component/controller/DashboardController.js b/src/javascripts/ng-admin/Main/component/controller/DashboardController.js index cf3aedf5..371c1596 100644 --- a/src/javascripts/ng-admin/Main/component/controller/DashboardController.js +++ b/src/javascripts/ng-admin/Main/component/controller/DashboardController.js @@ -13,11 +13,8 @@ define(function (require) { function DashboardController($scope, $state, PanelBuilder) { this.$scope = $scope; this.$state = $state; - this.PanelBuilder = PanelBuilder; - this.$scope.edit = this.edit.bind(this); - - this.retrievePanels(); + this.retrieveCollections(PanelBuilder); $scope.$on('$destroy', this.destroy.bind(this)); } @@ -25,36 +22,30 @@ define(function (require) { /** * Retrieve all dashboard panels */ - DashboardController.prototype.retrievePanels = function () { - var self = this; - this.panels = []; + DashboardController.prototype.retrieveCollections = function (PanelBuilder) { + this.collections = {}; + this.orderedCollections = []; var searchParams = this.$state.params; var sortField = 'sortField' in searchParams ? searchParams.sortField : null; var sortDir = 'sortDir' in searchParams ? searchParams.sortDir : null; - this.PanelBuilder.getPanelsData(sortField, sortDir).then(function (panels) { - self.panels = panels; - }); - this.hasEntities = this.PanelBuilder.hasEntities(); - }; - - /** - * Link to edit entity page - * - * @param {Entry} entry - */ - DashboardController.prototype.edit = function (entry) { - this.$state.go(this.$state.get('edit'), { - entity: entry.entityName, - id: entry.identifierValue + PanelBuilder.getPanelsData(sortField, sortDir).then(collections => { + this.collections = collections; + let orderedCollections = []; + for (var name in collections) { + orderedCollections.push(collections[name]); + } + this.orderedCollections = orderedCollections.sort((collectionA, collectionB) => { + return collectionA.order - collectionB.order; + }); }); + this.hasEntities = PanelBuilder.hasEntities(); }; DashboardController.prototype.destroy = function () { this.$scope = undefined; this.$state = undefined; - this.PanelBuilder = undefined; }; DashboardController.$inject = ['$scope', '$state', 'PanelBuilder']; diff --git a/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js b/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js index 1790b27f..a3c55184 100644 --- a/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js +++ b/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js @@ -8,8 +8,7 @@ function maDashboardPanel($state) { viewName: '@', entries: '=', fields: '&', - entity: '&', - perPage: '=' + entity: '&' }, link: function(scope) { scope.gotoList = function () { diff --git a/src/javascripts/ng-admin/Main/component/provider/NgAdminConfiguration.js b/src/javascripts/ng-admin/Main/component/provider/NgAdminConfiguration.js index 644848d2..bdb76192 100644 --- a/src/javascripts/ng-admin/Main/component/provider/NgAdminConfiguration.js +++ b/src/javascripts/ng-admin/Main/component/provider/NgAdminConfiguration.js @@ -41,6 +41,14 @@ NgAdminConfiguration.prototype.menu = function(entity) { return this.adminDescription.menu(entity); }; +NgAdminConfiguration.prototype.collection = function(collection) { + return this.adminDescription.collection(collection); +}; + +NgAdminConfiguration.prototype.dashboard = function(dashboard) { + return this.adminDescription.dashboard(dashboard); +}; + NgAdminConfiguration.$inject = ['$compileProvider']; module.exports = NgAdminConfiguration; diff --git a/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js b/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js index 799da7de..c0668871 100644 --- a/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js +++ b/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js @@ -23,45 +23,45 @@ PanelBuilder.prototype.hasEntities = function() { * @returns {promise} */ PanelBuilder.prototype.getPanelsData = function (sortField, sortDir) { - var dashboardViews = this.Configuration.getViewsOfType('DashboardView'), + var collections = this.Configuration.dashboard().collections(), dataStore = this.dataStore, - promises = [], - dashboardView, - dashboardSortField, - dashboardSortDir, + promises = {}, + collection, + collectionSortField, + collectionSortDir, self = this, - i; - - for (i in dashboardViews) { - dashboardView = dashboardViews[i]; - dashboardSortField = dashboardView.getSortFieldName(); - dashboardSortDir = dashboardView.sortDir(); - if (sortField && sortField.split('.')[0] === dashboardView.name()) { - dashboardSortField = sortField; - dashboardSortDir = sortDir; + collectionName; + + for (collectionName in collections) { + collection = collections[collectionName]; + collectionSortField = collection.getSortFieldName(); + collectionSortDir = collection.sortDir(); + if (sortField && sortField.split('.')[0] === collection.name()) { + collectionSortField = sortField; + collectionSortDir = sortDir; } - promises.push((function (dashboardView, dashboardSortField, dashboardSortDir) { + promises[collectionName] = (function (collection, collectionSortField, collectionSortDir) { var rawEntries, nonOptimizedReferencedData, optimizedReferencedData; return self.ReadQueries - .getAll(dashboardView, 1, {}, dashboardSortField, dashboardSortDir) + .getAll(collection, 1, {}, collectionSortField, collectionSortDir) .then(function (response) { rawEntries = response.data; return rawEntries; }) .then(function (rawEntries) { - return self.ReadQueries.getFilteredReferenceData(dashboardView.getNonOptimizedReferences(), rawEntries); + return self.ReadQueries.getFilteredReferenceData(collection.getNonOptimizedReferences(), rawEntries); }) .then(function (nonOptimizedReference) { nonOptimizedReferencedData = nonOptimizedReference; - return self.ReadQueries.getOptimizedReferencedData(dashboardView.getOptimizedReferences(), rawEntries); + return self.ReadQueries.getOptimizedReferencedData(collection.getOptimizedReferences(), rawEntries); }) .then(function (optimizedReference) { optimizedReferencedData = optimizedReference; - var references = dashboardView.getReferences(), + var references = collection.getReferences(), referencedData = angular.extend(nonOptimizedReferencedData, optimizedReferencedData), referencedEntries; @@ -81,50 +81,45 @@ PanelBuilder.prototype.getPanelsData = function (sortField, sortDir) { }) .then(function () { var entries = dataStore.mapEntries( - dashboardView.entity.name(), - dashboardView.identifier(), - dashboardView.getFields(), + collection.entity.name(), + collection.identifier(), + collection.getFields(), rawEntries ); // shortcut to diplay collection of entry with included referenced values - dataStore.fillReferencesValuesFromCollection(entries, dashboardView.getReferences(), true); + dataStore.fillReferencesValuesFromCollection(entries, collection.getReferences(), true); return { entries: entries }; }); - })(dashboardView, dashboardSortField, dashboardSortDir)); + })(collection, collectionSortField, collectionSortDir); } return this.$q.all(promises).then(function (responses) { - var i, - response, - view, + var collectionName, + collection, entity, - fields, - panels = []; - - for (i in responses) { - response = responses[i]; - view = dashboardViews[i]; - entity = view.getEntity(); - fields = view.fields(); - - panels.push({ - label: view.title() || view.getEntity().label(), - viewName: view.name(), - fields: fields, + collectionData = {}; + + for (collectionName in responses) { + collection = collections[collectionName]; + entity = collection.getEntity(); + collectionData[collectionName] = { + label: collection.title() || entity.label(), + viewName: collection.name(), + fields: collection.fields(), entity: entity, - perPage: view.perPage(), - entries: response.entries, - sortField: view.getSortFieldName(), - sortDir: view.sortDir() - }); + order: collection.order(), + entries: responses[collectionName].entries, + sortField: collection.getSortFieldName(), + sortDir: collection.sortDir() + }; } - return panels; + return collectionData; }); }; diff --git a/src/javascripts/ng-admin/Main/config/routing.js b/src/javascripts/ng-admin/Main/config/routing.js index f294aceb..1764db57 100644 --- a/src/javascripts/ng-admin/Main/config/routing.js +++ b/src/javascripts/ng-admin/Main/config/routing.js @@ -22,7 +22,9 @@ function routing($stateProvider, $urlRouterProvider) { }, controller: 'DashboardController', controllerAs: 'dashboardController', - template: dashboardTemplate + templateProvider: ['NgAdminConfiguration', function(Configuration) { + return Configuration().dashboard().template() || dashboardTemplate; + }] }); $stateProvider.state('ma-404', { diff --git a/src/javascripts/ng-admin/Main/view/dashboard.html b/src/javascripts/ng-admin/Main/view/dashboard.html index 912d958e..6664cc1e 100644 --- a/src/javascripts/ng-admin/Main/view/dashboard.html +++ b/src/javascripts/ng-admin/Main/view/dashboard.html @@ -8,28 +8,26 @@

Dashboard

-
- +
+
-
- +
+
From 5356efc038cbbf23331a3f775a1439eb3444d2fe Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Wed, 17 Jun 2015 08:45:36 +0200 Subject: [PATCH 02/10] Add doc --- README.md | 82 +++++++++++++--------- doc/Dashboard.md | 173 +++++++++++++++++++++++++++++++++++++++++++++++ doc/Theming.md | 31 ++++++--- 3 files changed, 245 insertions(+), 41 deletions(-) create mode 100644 doc/Dashboard.md diff --git a/README.md b/README.md index b2d7cf0a..ababc67f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Check out the [online demo](http://ng-admin.marmelab.com/) ([source](https://git * [Entity Configuration](#entity-configuration) * [View Configuration](#view-configuration) * [Menu Configuration](#menu-configuration) +* [Dashboard Configuration](doc/Dashboard.md) * [Reusable Directives](#reusable-directives) * [Relationships](#relationships) * [Customizing the API Mapping](doc/API-mapping.md) @@ -84,7 +85,7 @@ If you rather like to embed dependencies separately, here is a snippet showing a Make your application depend on ng-admin: ```js -var app = angular.module('myApp', ['ng-admin']); +var admin = angular.module('myApp', ['ng-admin']); ``` First step is to map ng-admin entities to your API: @@ -93,15 +94,14 @@ First step is to map ng-admin entities to your API: app.config(function (NgAdminConfigurationProvider) { var nga = NgAdminConfigurationProvider; // set the main API endpoint for this admin - var app = nga.application('My backend') + var admin = nga.application('My backend') .baseApiUrl('http://localhost:3000/'); // define an entity mapped by the http://localhost:3000/posts endpoint var post = nga.entity('posts'); - app.addEntity(post); + admin.addEntity(post); // set the list of fields to map in each post view - post.dashboardView().fields(/* see example below */); post.listView().fields(/* see example below */); post.creationView().fields(/* see example below */); post.editionView().fields(/* see example below */); @@ -137,7 +137,7 @@ Here is a full example for a backend that will let you create, update, and delet var app = angular.module('myApp', ['ng-admin']); -app.config(function (NgAdminConfigurationProvider) { +admin.config(function (NgAdminConfigurationProvider) { var nga = NgAdminConfigurationProvider; var app = nga.application('ng-admin backend demo', false) // application main title and debug disabled .baseApiUrl('http://localhost:3000/'); // main API endpoint @@ -147,16 +147,9 @@ app.config(function (NgAdminConfigurationProvider) { .identifier(nga.field('id')); // you can optionally customize the identifier used in the api ('id' by default) // set the application entities - app.addEntity(post); + admin.addEntity(post); // customize entities and views - - post.dashboardView() // customize the dashboard panel for this entity - .title('Recent posts') - .order(1) // display the post panel first in the dashboard - .perPage(5) // limit the panel to the 5 latest posts - .fields([nga.field('title').isDetailLink(true).map(truncate)]); // fields() called with arguments add fields to the view - post.listView() .title('All posts') // default title is "[Entity_name] list" .description('List of posts with infinite pagination') // description appears under the title @@ -281,14 +274,13 @@ Defines the API endpoint for all views of this entity. It can be a string or a f ### View Types -Each entity has 6 views that you can customize: +Each entity has 5 views that you can customize: - `listView` - `creationView` - `editionView` - `showView` (unused by default) - `deletionView` -- `dashboardView`: another special view to define a panel in the dashboard (the ng-admin homepage) for an entity. ### General View Settings @@ -327,7 +319,7 @@ Customize the list of actions for this view. You can pass a list of button names editionView.actions(template); * `disable()` -Disable this view. Useful e.g. to hide the panel for one entity in the dashboard, or to disable views that modify data and only let the `listView` enabled +Disable this view. Useful e.g. to disable views that modify data and only leave the `listView` enabled * `url()` Defines the API endpoint for a view. It can be a string or a function. @@ -336,16 +328,6 @@ Defines the API endpoint for a view. It can be a string or a function. return '/comments/id/' + entityId; // Can be absolute or relative }); -### dashboardView Settings - -The `dashboardView` also defines `sortField` and `sortDir` fields like the `listView`. - -* `perPage(Number)` -Set the number of items. - -* `order(Number)` -Define the order of the Dashboard panel for this entity in the dashboard - ### listView Settings * `perPage(Number)` @@ -570,7 +552,7 @@ By default, ng-admin creates a sidebar menu with one entry per entity. If you wa The sidebar menu is built based on a `Menu` object, constructed with `nga.menu()`. A menu can have child menus. A menu can be constructed based on an entity. Here is the code to create a basic menu for the entities `post`, `comment`, and `tag`: ```js -app.menu(nga.menu() +admin.menu(nga.menu() .addChild(nga.menu(post)) .addChild(nga.menu(comment)) .addChild(nga.menu(tag)) @@ -580,7 +562,7 @@ app.menu(nga.menu() The menus appear in the order in which they were added to the main menu. The `Menu` class offers `icon()`, `title()`, and `template()` methods to customize how the menu renders. ```js -app.menu(nga.menu() +admin.menu(nga.menu() .addChild(nga.menu(post)) .addChild(nga.menu(comment).title('Comments')) .addChild(nga.menu(tag).icon('')) @@ -590,7 +572,7 @@ app.menu(nga.menu() You can also choose to define a menu from scratch. In this case, you should define the internal state the menu points to using `link()`, and the function to determine whether the menu is active based on the current state with `active()`. ```js -app.menu(nga.menu() +admin.menu(nga.menu() .addChild(nga.menu() .title('Stats') .link('/stats') @@ -604,22 +586,28 @@ app.menu(nga.menu() You can add also second-level menus. ```js -app.menu(nga.menu() +admin.menu(nga.menu() .addChild(nga.menu().title('Miscellaneous') .addChild(nga.menu().title('Stats').link('/stats')) ) ); ``` -*Tip*: `app.menu()` is both a setter and a getter. You can modify an existing menu in the admin configuration by using `app.menu().getChildByTitle()` +*Tip*: `admin.menu()` is both a setter and a getter. You can modify an existing menu in the admin configuration by using `admin.menu().getChildByTitle()` ```js -app.addEntity(post) -app.menu().getChildByTitle('Post') +admin.addEntity(post) +admin.menu().getChildByTitle('Post') .title('Posts') .icon(''); ``` +## Dashboard Configuration + +The home page of a ng-admin application is called the Dashboard. Use it to show important pieces of information to the end user, such as latest entries, or charts. + +See [Dashboard Configuration](doc/Dashboard.md) dedicated chapter. + ## Reusable Directives The `template` field type allows you to use any HTML tag, including custom directives. ng-admin provides ready-to-use directives to easily add interactions to your admin views: @@ -793,6 +781,34 @@ Define a function that returns parameters for filtering API calls. You can use i }) ]); +## Customizing the API Mapping + +All HTTP requests made by ng-admin to your REST API are carried out by [Restangular](https://github.com/mgonto/restangular), which is like `$resource` on steroids. + +The REST specification doesn't provide enough detail to cover all requirements of an administration GUI. ng-admin makes some assumptions about how your API is designed. All of these assumptions can be overridden by way of [Restangular's request and response interceptors](https://github.com/mgonto/restangular#addresponseinterceptor). + +That means you don't need to adapt your API to ng-admin; ng-admin can adapt to any REST API, thanks to the flexibility of Restangular. + +See the [Customizing the API Mapping](doc/API-mapping.md) dedicated chapter. + +## Theming + +You can override pretty much all the HTML generated by ng-admin, at different levels. + +See the [Theming](doc/Theming.md) dedicated chapter. + +## Adding Custom Pages + +For each entity, ng-admin creates the necessary pages for Creating, Retieving, Updating, and Deleting (CRUD) this entity. When you need to achieve more specific actions on an entity, you have to add a custom page - for instance a page asking for an email address to send a message to. How can you route to a specific page and display it in the ng-admin layout? + +See the [Adding Custom Pages](doc/Custom-pages.md) dedicated chapter. + +## Adding Custom Types + +When you map a field between a REST API response and ng-admin, you give it a type. This type determines how the data is displayed and edited. It is very easy to customize existing ng-admin types and add new ones. + +See the [Adding Custom Types](doc/Custom-types.md) dedicated chapter. + ## Contributing Your feedback about the usage of ng-admin in your specific context is valuable, don't hesitate to [open GitHub Issues](https://github.com/marmelab/ng-admin/issues) for any problem or question you may have. diff --git a/doc/Dashboard.md b/doc/Dashboard.md new file mode 100644 index 00000000..07b66c94 --- /dev/null +++ b/doc/Dashboard.md @@ -0,0 +1,173 @@ +# Customizing the Dashboard + +The home page of a ng-admin application is called the Dashboard. Use it to show important pieces of information to the end user, such as latest entries, or charts. + +## Default Dashboard + +By default, the dashboard page shows one panel per entity, ordered from left to right according to the order in which the entities were added to the application. + +Each panel contains a list of latest entries for the entity, based on the fields definition of the `listView` , and limited to 3 fields to avoid screen clutter. + +## Dashboard Configuration + +If the default Dashboard doesn't suit your needs, create a custom dashboard configuration: + +```js +admin.dashboard(nga.dashboard() + .addCollection(name, collection) + .addCollection(name, collection) + .addCollection(name, collection) +); +``` + +The dashboard configuration defines the dashboard datasources, using named `Collection` definitions. Collection names are useful when you use a custom dashboard template (see below). To create a collection, simply use `nga.collection()`. + +On each collection, you can define the same type of settings as in a `listView`: title, fields, numer of rows (`perPage()`), sorting order (`sortField()` and `sortDir()`). In addition, you can customize the position of the collection in the dashboard using the `order()` function: + +```js +admin.dashboard(nga.dashboard() + .addCollection('posts', nga.collection(post) + .title('Recent posts') + .perPage(5) // limit the panel to the 5 latest posts + .fields([ + nga.field('title').isDetailLink(true).map(truncate) + ]) + .sortField('id') + .sortOrder('DESC') + .order(1) + ) + .addCollection('comments', nga.collection(comment) + .title('Last comments') + .perPage(5) + .fields([ + nga.field('id'), + nga.field('body', 'wysiwyg').label('Comment').stripTags(true).map(truncate), + nga.field(null, 'template').label('').template('') // you can use custom directives, too + ]) + .order(2) + ) + .addCollection('tags', nga.collection(tag) + .title('Recent tags') + .perPage(10) + .fields([ + nga.field('id'), + nga.field('name'), + nga.field('published', 'boolean').label('Is published ?') + ]) + .order(3) + ) +); +``` + +You can add any number of collections you want, even several collections per entity (e.g. to show most commented posts and posts waiting for publication). + +As soon as you define a custom dashboard configuration, it will replace the default dashboard based on all entities and listViews. + +If you just need to remove one panel from the default dasboard layout, you have to write a custom dashboard configuration from the ground up. + +## Dashboard Template + +The default dashboard template iterates over all the collections, and displayes them from left to righ and from top to bottom. If you want to change the size of a particular dashboard panel, or if you want to add panels other than a datagrid, you have to write the dashboard template and set it in the dashboard: + +```js +admin.dashboard(nga.dashboard() + .template(templateString) +); +``` + +Here is a copy of the default dashboard template, which you can use as a starting point for a custom dashboard template: + +```html +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+``` + +If you decide to use a custom template, you should probably remove the `ng-repeat` and insert the collections one by one, using their names. This way, you can achieve a completely custom layout, and even include custom directives like charts. In that case, don't bother to define `title()` and `order()` in the collection, since you can more easily write them down in the template. + +```html +
+
+ +
+
+ +
+
+
+ + + + +
+
+
+ + + + +
+
+
+
+
+ + + + +
+
+
+``` + diff --git a/doc/Theming.md b/doc/Theming.md index f12af980..3b5db401 100644 --- a/doc/Theming.md +++ b/doc/Theming.md @@ -66,7 +66,8 @@ var myEntity = nga.entity('foo_endpoint'); myEntity.listView().template(myTemplate); // continue myEntity configuration // ... -app.addEntity(myEntity); +var admin = nga.application('My Application'); +admin.addEntity(myEntity); ``` ## Customizing The View Templates For The Entire Application @@ -75,16 +76,30 @@ You can use the `app.customTemplate()` method to customize the template of a giv ```js var myTemplate = require('text!./path/to/list.html'); -app.customTemplate(function(viewName) { +var admin = nga.application('My Application'); +admin.customTemplate(function(viewName) { if (viewName === 'ListView') { return myTemplate; } }) ``` +## Customizing the Dashboard + +If you want to use a custom template dashboard, you must [define a custom dashboard configuration](doc/Dashboard.md). This will give you access to the `dashboard.template()` function: + +```js +var myTemplate = require('text!./path/to/dashboard.html'); +var admin = nga.application('My Application'); +admin.dashboard(nga.dashboard() + .template(mytemplate) +}) +``` + + ## Customizing the Application Header -If you want to override the application header (for instance to add authentication status on the top bar), use the `app.header()` setter with a valid HTML content. Angular directives are executed in the context of the layout. For instance, to display a custom title and a right aligned link, use the following code: +If you want to override the application header (for instance to add authentication status on the top bar), use the `admin.header()` setter with a valid HTML content. Angular directives are executed in the context of the layout. For instance, to display a custom title and a right aligned link, use the following code: ```js var customHeaderTemplate = @@ -98,18 +113,18 @@ var customHeaderTemplate = ' View Source' + '' + '

'; -var app = nga.application('My Application'); -app.header(customHeaderTemplate); +var admin = nga.application('My Application'); +admin.header(customHeaderTemplate); ``` ## Customizing the Application Layout -If the header is not enough, and you need to override the entire application layout use the `app.layout()` setter: +If the header is not enough, and you need to override the entire application layout use the `admin.layout()` setter: ```js var myLayout = require('text!./path/to/layout.html'); -var app = nga.application('My Application'); -app.layout(myLayout); +var admin = nga.application('My Application'); +admin.layout(myLayout); ``` The original layout can be found in [src/javascripts/ng-admin/Main/view/layout.html](../src/javascripts/ng-admin/Main/view/layout.html). From c145cb5b9d5118055b00fc2467acc1cb143432b4 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Wed, 17 Jun 2015 08:51:22 +0200 Subject: [PATCH 03/10] deprecate dashboardView() --- UPGRADE-0.8.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/UPGRADE-0.8.md b/UPGRADE-0.8.md index 7fc9bd0f..ab0866cc 100644 --- a/UPGRADE-0.8.md +++ b/UPGRADE-0.8.md @@ -20,3 +20,56 @@ The `Field.identifier(true)` does not exist anymore. Instead, you must specify t + entity.identifier(nga.field('id')) ``` +## `dashboardView()` is deprecated, use `admin.dashboard()` instead + +To let developers customize the admin dashboard at will, ng-admin 0.8 decouples the dashboard data and presentation. You can setup the dashboard datasources and template on a new member of ythe admin class, `dahboard()`: + +```js +admin.dashboard(nga.dashboard() + .addCollection(name, collection) + .addCollection(name, collection) + .addCollection(name, collection) + .template(templateString) +); +``` + +This is the preferred way to customize dashboard panel position, title, and to customize the fields displayed in each panel. Configure a collection just like you would like to configure a `listView`. For instance: + +```js +admin.dashboard(nga.dashboard() + .addCollection('posts', nga.collection(post) + .title('Recent posts') + .perPage(5) // limit the panel to the 5 latest posts + .fields([ + nga.field('title').isDetailLink(true).map(truncate) + ]) + .sortField('id') + .sortOrder('DESC') + .order(1) + ) + .addCollection('comments', nga.collection(comment) + .title('Last comments') + .perPage(5) + .fields([ + nga.field('id'), + nga.field('body', 'wysiwyg').label('Comment').stripTags(true).map(truncate), + nga.field(null, 'template').label('').template('') // you can use custom directives, too + ]) + .order(2) + ) + .addCollection('tags', nga.collection(tag) + .title('Recent tags') + .perPage(10) + .fields([ + nga.field('id'), + nga.field('name'), + nga.field('published', 'boolean').label('Is published ?') + ]) + .order(3) + ) +); +``` + +See the [Dashboard Configuration](doc/Dashboard.md) dedicated chapter for more details. + +Calls to `dashboardView()` are still supported in ng-admin 0.8, but will raise an error in future versions. From 795627df7fa203498efb265017aae49c68e1efaa Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Wed, 17 Jun 2015 19:22:09 +0200 Subject: [PATCH 04/10] Simplify panel markup --- doc/Dashboard.md | 71 +++---------------- package.json | 2 +- .../controller/DashboardController.js | 24 +++---- .../component/directive/maDashboardPanel.js | 10 ++- .../Main/component/filter/OrderElement.js | 2 +- .../Main/component/service/PanelBuilder.js | 34 +++------ .../ng-admin/Main/view/dashboard-panel.html | 10 +-- .../ng-admin/Main/view/dashboard.html | 22 ++---- 8 files changed, 46 insertions(+), 129 deletions(-) diff --git a/doc/Dashboard.md b/doc/Dashboard.md index 07b66c94..df1d7db2 100644 --- a/doc/Dashboard.md +++ b/doc/Dashboard.md @@ -88,27 +88,13 @@ Here is a copy of the default dashboard template, which you can use as a startin
-
- - +
+
-
- - +
+
@@ -117,57 +103,18 @@ Here is a copy of the default dashboard template, which you can use as a startin If you decide to use a custom template, you should probably remove the `ng-repeat` and insert the collections one by one, using their names. This way, you can achieve a completely custom layout, and even include custom directives like charts. In that case, don't bother to define `title()` and `order()` in the collection, since you can more easily write them down in the template. ```html -
-
- -
-
-
-
- - - - +
+
-
- - - - -
-
-
-
- - - - +
+
+ ``` diff --git a/package.json b/package.json index 2f82b67e..0caf3a1b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "npm": "^2.10.0" }, "devDependencies": { - "admin-config": "^0.1.9", + "admin-config": "^0.2.0", "angular": "~1.3.15", "angular-bootstrap": "^0.12.0", "angular-mocks": "^1.3.15", diff --git a/src/javascripts/ng-admin/Main/component/controller/DashboardController.js b/src/javascripts/ng-admin/Main/component/controller/DashboardController.js index 371c1596..4922859e 100644 --- a/src/javascripts/ng-admin/Main/component/controller/DashboardController.js +++ b/src/javascripts/ng-admin/Main/component/controller/DashboardController.js @@ -14,31 +14,29 @@ define(function (require) { this.$scope = $scope; this.$state = $state; - this.retrieveCollections(PanelBuilder); + this.retrieveCollectionsAndData(PanelBuilder); $scope.$on('$destroy', this.destroy.bind(this)); } + DashboardController.prototype.gotoList = function(entityName) { + this.$state.go(this.$state.get('list'), { entity: entityName }); + } + /** * Retrieve all dashboard panels */ - DashboardController.prototype.retrieveCollections = function (PanelBuilder) { - this.collections = {}; - this.orderedCollections = []; + DashboardController.prototype.retrieveCollectionsAndData = function (PanelBuilder) { + let collections = PanelBuilder.getCollections(); + this.entries = {}; var searchParams = this.$state.params; var sortField = 'sortField' in searchParams ? searchParams.sortField : null; var sortDir = 'sortDir' in searchParams ? searchParams.sortDir : null; - PanelBuilder.getPanelsData(sortField, sortDir).then(collections => { - this.collections = collections; - let orderedCollections = []; - for (var name in collections) { - orderedCollections.push(collections[name]); - } - this.orderedCollections = orderedCollections.sort((collectionA, collectionB) => { - return collectionA.order - collectionB.order; - }); + PanelBuilder.getEntries(sortField, sortDir).then(entries => { + this.entries = entries; + this.collections = collections; // we set collectrions only when entries are fetched to avoid double draw }); this.hasEntities = PanelBuilder.hasEntities(); }; diff --git a/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js b/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js index a3c55184..6e1bc012 100644 --- a/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js +++ b/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js @@ -4,15 +4,13 @@ function maDashboardPanel($state) { return { restrict: 'E', scope: { - label: '@', - viewName: '@', - entries: '=', - fields: '&', - entity: '&' + collection: '&', + entries: '&' }, link: function(scope) { + scope.collection = scope.collection(); scope.gotoList = function () { - $state.go($state.get('list'), { entity: scope.entity().name() }); + $state.go($state.get('list'), { entity: scope.collection.entity.name() }); }; }, template: dashboardPanelView diff --git a/src/javascripts/ng-admin/Main/component/filter/OrderElement.js b/src/javascripts/ng-admin/Main/component/filter/OrderElement.js index cfa70c60..1732e77b 100644 --- a/src/javascripts/ng-admin/Main/component/filter/OrderElement.js +++ b/src/javascripts/ng-admin/Main/component/filter/OrderElement.js @@ -13,7 +13,7 @@ define(function () { } results.sort(function (field1, field2) { - return field1.order() - field2.order(); + return typeof field1.order === 'function' ? field1.order() - field2.order() : field1.order - field2.order; }); return results; diff --git a/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js b/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js index c0668871..6f130a2d 100644 --- a/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js +++ b/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js @@ -17,13 +17,17 @@ PanelBuilder.prototype.hasEntities = function() { return this.Configuration.entities.length > 0; } +PanelBuilder.prototype.getCollections = function (sortField, sortDir) { + return this.Configuration.dashboard().collections(); +} + /** - * Returns all elements of each dashboard panels + * Returns all entries for all collections * * @returns {promise} */ -PanelBuilder.prototype.getPanelsData = function (sortField, sortDir) { - var collections = this.Configuration.dashboard().collections(), +PanelBuilder.prototype.getEntries = function (sortField, sortDir) { + var collections = this.getCollections(), dataStore = this.dataStore, promises = {}, collection, @@ -87,39 +91,23 @@ PanelBuilder.prototype.getPanelsData = function (sortField, sortDir) { rawEntries ); - // shortcut to diplay collection of entry with included referenced values dataStore.fillReferencesValuesFromCollection(entries, collection.getReferences(), true); - return { - entries: entries - }; + return entries; }); })(collection, collectionSortField, collectionSortDir); } return this.$q.all(promises).then(function (responses) { var collectionName, - collection, - entity, - collectionData = {}; + entries = {}; for (collectionName in responses) { - collection = collections[collectionName]; - entity = collection.getEntity(); - collectionData[collectionName] = { - label: collection.title() || entity.label(), - viewName: collection.name(), - fields: collection.fields(), - entity: entity, - order: collection.order(), - entries: responses[collectionName].entries, - sortField: collection.getSortFieldName(), - sortDir: collection.sortDir() - }; + entries[collections[collectionName].name()] = responses[collectionName]; } - return collectionData; + return entries; }); }; diff --git a/src/javascripts/ng-admin/Main/view/dashboard-panel.html b/src/javascripts/ng-admin/Main/view/dashboard-panel.html index 9779edc4..d980ddc8 100644 --- a/src/javascripts/ng-admin/Main/view/dashboard-panel.html +++ b/src/javascripts/ng-admin/Main/view/dashboard-panel.html @@ -1,10 +1,10 @@ - diff --git a/src/javascripts/ng-admin/Main/view/dashboard.html b/src/javascripts/ng-admin/Main/view/dashboard.html index 6664cc1e..d5a1e81e 100644 --- a/src/javascripts/ng-admin/Main/view/dashboard.html +++ b/src/javascripts/ng-admin/Main/view/dashboard.html @@ -8,27 +8,13 @@

Dashboard

-
- - +
+
-
- - +
+
From 733a3ee1e19c98f8900ee40a18233f843081e2e9 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Wed, 17 Jun 2015 21:47:41 +0200 Subject: [PATCH 05/10] move PanelBuilder logic to routing --- src/javascripts/ng-admin/Main/MainModule.js | 2 - .../controller/DashboardController.js | 29 +---- .../Main/component/service/PanelBuilder.js | 116 ------------------ .../ng-admin/Main/config/routing.js | 102 ++++++++++++++- .../component/service/PanelBuilderSpec.js | 98 --------------- 5 files changed, 106 insertions(+), 241 deletions(-) delete mode 100644 src/javascripts/ng-admin/Main/component/service/PanelBuilder.js delete mode 100644 src/javascripts/test/unit/Main/component/service/PanelBuilderSpec.js diff --git a/src/javascripts/ng-admin/Main/MainModule.js b/src/javascripts/ng-admin/Main/MainModule.js index 31e1d6cb..164a1422 100644 --- a/src/javascripts/ng-admin/Main/MainModule.js +++ b/src/javascripts/ng-admin/Main/MainModule.js @@ -8,8 +8,6 @@ var MainModule = angular.module('main', ['ui.router', 'restangular']); MainModule.controller('AppController', require('./component/controller/AppController')); MainModule.controller('DashboardController', require('./component/controller/DashboardController')); -MainModule.service('PanelBuilder', require('./component/service/PanelBuilder')); - MainModule.provider('NgAdminConfiguration', require('./component/provider/NgAdminConfiguration')); MainModule.filter('orderElement', require('./component/filter/OrderElement')); diff --git a/src/javascripts/ng-admin/Main/component/controller/DashboardController.js b/src/javascripts/ng-admin/Main/component/controller/DashboardController.js index 4922859e..edada82d 100644 --- a/src/javascripts/ng-admin/Main/component/controller/DashboardController.js +++ b/src/javascripts/ng-admin/Main/component/controller/DashboardController.js @@ -10,11 +10,11 @@ define(function (require) { * @param {PanelBuilder} PanelBuilder * @constructor */ - function DashboardController($scope, $state, PanelBuilder) { - this.$scope = $scope; + function DashboardController($scope, $state, collections, entries, hasEntities) { this.$state = $state; - - this.retrieveCollectionsAndData(PanelBuilder); + this.collections = collections; + this.entries = entries; + this.hasEntities = hasEntities; $scope.$on('$destroy', this.destroy.bind(this)); } @@ -23,30 +23,11 @@ define(function (require) { this.$state.go(this.$state.get('list'), { entity: entityName }); } - /** - * Retrieve all dashboard panels - */ - DashboardController.prototype.retrieveCollectionsAndData = function (PanelBuilder) { - let collections = PanelBuilder.getCollections(); - this.entries = {}; - - var searchParams = this.$state.params; - var sortField = 'sortField' in searchParams ? searchParams.sortField : null; - var sortDir = 'sortDir' in searchParams ? searchParams.sortDir : null; - - PanelBuilder.getEntries(sortField, sortDir).then(entries => { - this.entries = entries; - this.collections = collections; // we set collectrions only when entries are fetched to avoid double draw - }); - this.hasEntities = PanelBuilder.hasEntities(); - }; - DashboardController.prototype.destroy = function () { - this.$scope = undefined; this.$state = undefined; }; - DashboardController.$inject = ['$scope', '$state', 'PanelBuilder']; + DashboardController.$inject = ['$scope', '$state', 'collections', 'entries', 'hasEntities']; return DashboardController; }); diff --git a/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js b/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js deleted file mode 100644 index 6f130a2d..00000000 --- a/src/javascripts/ng-admin/Main/component/service/PanelBuilder.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @param {$q} $q - * @param {ReadQueries} ReadQueries - * @param {Configuration} Configuration - * @param {AdminDescription} AdminDescription - * - * @constructor - */ -function PanelBuilder($q, ReadQueries, Configuration, AdminDescription) { - this.$q = $q; - this.ReadQueries = ReadQueries; - this.dataStore = AdminDescription.getDataStore(); - this.Configuration = Configuration(); -} - -PanelBuilder.prototype.hasEntities = function() { - return this.Configuration.entities.length > 0; -} - -PanelBuilder.prototype.getCollections = function (sortField, sortDir) { - return this.Configuration.dashboard().collections(); -} - -/** - * Returns all entries for all collections - * - * @returns {promise} - */ -PanelBuilder.prototype.getEntries = function (sortField, sortDir) { - var collections = this.getCollections(), - dataStore = this.dataStore, - promises = {}, - collection, - collectionSortField, - collectionSortDir, - self = this, - collectionName; - - for (collectionName in collections) { - collection = collections[collectionName]; - collectionSortField = collection.getSortFieldName(); - collectionSortDir = collection.sortDir(); - if (sortField && sortField.split('.')[0] === collection.name()) { - collectionSortField = sortField; - collectionSortDir = sortDir; - } - promises[collectionName] = (function (collection, collectionSortField, collectionSortDir) { - var rawEntries, nonOptimizedReferencedData, optimizedReferencedData; - - return self.ReadQueries - .getAll(collection, 1, {}, collectionSortField, collectionSortDir) - .then(function (response) { - rawEntries = response.data; - - return rawEntries; - }) - .then(function (rawEntries) { - return self.ReadQueries.getFilteredReferenceData(collection.getNonOptimizedReferences(), rawEntries); - }) - .then(function (nonOptimizedReference) { - nonOptimizedReferencedData = nonOptimizedReference; - - return self.ReadQueries.getOptimizedReferencedData(collection.getOptimizedReferences(), rawEntries); - }) - .then(function (optimizedReference) { - optimizedReferencedData = optimizedReference; - - var references = collection.getReferences(), - referencedData = angular.extend(nonOptimizedReferencedData, optimizedReferencedData), - referencedEntries; - - for (var name in referencedData) { - referencedEntries = dataStore.mapEntries( - references[name].targetEntity().name(), - references[name].targetEntity().identifier(), - [references[name].targetField()], - referencedData[name] - ); - - dataStore.setEntries( - references[name].targetEntity().uniqueId + '_values', - referencedEntries - ); - } - }) - .then(function () { - var entries = dataStore.mapEntries( - collection.entity.name(), - collection.identifier(), - collection.getFields(), - rawEntries - ); - - // shortcut to diplay collection of entry with included referenced values - dataStore.fillReferencesValuesFromCollection(entries, collection.getReferences(), true); - - return entries; - }); - })(collection, collectionSortField, collectionSortDir); - } - - return this.$q.all(promises).then(function (responses) { - var collectionName, - entries = {}; - - for (collectionName in responses) { - entries[collections[collectionName].name()] = responses[collectionName]; - } - - return entries; - }); -}; - -PanelBuilder.$inject = ['$q', 'ReadQueries', 'NgAdminConfiguration', 'AdminDescription']; - -module.exports = PanelBuilder; diff --git a/src/javascripts/ng-admin/Main/config/routing.js b/src/javascripts/ng-admin/Main/config/routing.js index 1764db57..a672090b 100644 --- a/src/javascripts/ng-admin/Main/config/routing.js +++ b/src/javascripts/ng-admin/Main/config/routing.js @@ -2,6 +2,12 @@ var layoutTemplate = require('../view/layout.html'), dashboardTemplate = require('../view/dashboard.html'), errorTemplate = require('../view/404.html'); +function dataStoreProvider() { + return ['AdminDescription', function (AdminDescription) { + return AdminDescription.getDataStore(); + }]; +} + function routing($stateProvider, $urlRouterProvider) { $stateProvider.state('main', { @@ -24,7 +30,101 @@ function routing($stateProvider, $urlRouterProvider) { controllerAs: 'dashboardController', templateProvider: ['NgAdminConfiguration', function(Configuration) { return Configuration().dashboard().template() || dashboardTemplate; - }] + }], + resolve: { + dataStore: dataStoreProvider(), + hasEntities: ['NgAdminConfiguration', function(Configuration) { + return Configuration().entities.length > 0; + }], + collections: ['NgAdminConfiguration', function(Configuration) { + return Configuration().dashboard().collections(); + }], + responses: ['$stateParams', '$q', 'collections', 'dataStore', 'ReadQueries', function($stateParams, $q, collections, dataStore, ReadQueries) { + var sortField = 'sortField' in $stateParams ? $stateParams.sortField : null; + var sortDir = 'sortDir' in $stateParams ? $stateParams.sortDir : null; + + var promises = {}, + collection, + collectionSortField, + collectionSortDir, + collectionName; + + for (collectionName in collections) { + collection = collections[collectionName]; + collectionSortField = collection.getSortFieldName(); + collectionSortDir = collection.sortDir(); + if (sortField && sortField.split('.')[0] === collection.name()) { + collectionSortField = sortField; + collectionSortDir = sortDir; + } + promises[collectionName] = (function (collection, collectionSortField, collectionSortDir) { + var rawEntries, nonOptimizedReferencedData, optimizedReferencedData; + + return ReadQueries + .getAll(collection, 1, {}, collectionSortField, collectionSortDir) + .then(function (response) { + rawEntries = response.data; + + return rawEntries; + }) + .then(function (rawEntries) { + return ReadQueries.getFilteredReferenceData(collection.getNonOptimizedReferences(), rawEntries); + }) + .then(function (nonOptimizedReference) { + nonOptimizedReferencedData = nonOptimizedReference; + + return ReadQueries.getOptimizedReferencedData(collection.getOptimizedReferences(), rawEntries); + }) + .then(function (optimizedReference) { + optimizedReferencedData = optimizedReference; + + var references = collection.getReferences(), + referencedData = angular.extend(nonOptimizedReferencedData, optimizedReferencedData), + referencedEntries; + + for (var name in referencedData) { + referencedEntries = dataStore.mapEntries( + references[name].targetEntity().name(), + references[name].targetEntity().identifier(), + [references[name].targetField()], + referencedData[name] + ); + + dataStore.setEntries( + references[name].targetEntity().uniqueId + '_values', + referencedEntries + ); + } + }) + .then(function () { + var entries = dataStore.mapEntries( + collection.entity.name(), + collection.identifier(), + collection.getFields(), + rawEntries + ); + + // shortcut to diplay collection of entry with included referenced values + dataStore.fillReferencesValuesFromCollection(entries, collection.getReferences(), true); + + return entries; + }); + })(collection, collectionSortField, collectionSortDir); + } + + return $q.all(promises) + }], + entries: ['responses', 'collections', function(responses, collections) { + var collectionName, + entries = {}; + + for (collectionName in responses) { + entries[collections[collectionName].name()] = responses[collectionName]; + } + + return entries; + }] + } }); $stateProvider.state('ma-404', { diff --git a/src/javascripts/test/unit/Main/component/service/PanelBuilderSpec.js b/src/javascripts/test/unit/Main/component/service/PanelBuilderSpec.js deleted file mode 100644 index 91a26d16..00000000 --- a/src/javascripts/test/unit/Main/component/service/PanelBuilderSpec.js +++ /dev/null @@ -1,98 +0,0 @@ -/*global jasmine,angular,describe,it,expect*/ -var PanelBuilder = require('../../../../../ng-admin/Main/component/service/PanelBuilder'), - Field = require('admin-config/lib/Field/Field'), - DashboardView = require('admin-config/lib/View/DashboardView'), - Entity = require('admin-config/lib/Entity/Entity'), - DataStore = require('admin-config/lib/DataStore/DataStore'), - mixins = require('../../../../mock/mixins'); - -describe("PanelBuilder", function () { - 'use strict'; - - describe('getPanelsData', function() { - - it('should retrieve panels', function (done) { - var entity = new Entity('myEntity'), - view1 = new DashboardView('view1') - .title('dashboard1') - .setEntity(entity) - .addField(new Field('title').label('Title')), - view2 = new DashboardView('MyView2') - .title('my dashboard 2') - .setEntity(entity) - .addField(new Field('name').label('Name')); - - var responses = [ - { - view: view1, - data: [], - currentPage: 1, - perPage: 10, - totalItems: 12 - }, - { - view: view2, - data: [], - currentPage: 1, - perPage: 10, - totalItems: 4 - } - ]; - - var panelBuilder = getPanelBuilder([view1, view2], responses); - panelBuilder.getPanelsData().then(function(panels) { - // Check that panels are retrieved - expect(panels[0].label).toEqual('dashboard1'); - expect(panels[1].label).toEqual('my dashboard 2'); - - expect(Object.keys(panels[1].fields).length).toEqual(1); - expect(panels[1].fields[0].label()).toEqual('Name'); - expect(panels[1].fields[0].name()).toEqual('name'); - }) - .finally(done); - - }); - - it('should default to entity label if no title is provided', function(done) { - var dashboardView = new DashboardView('view1').addField(new Field('title').label('Title')); - dashboardView.setEntity(new Entity('MyEntity')); - - var response = { - view: dashboardView, - data: [], - currentPage: 1, - perPage: 10, - totalItems: 12 - }; - - var panelBuilder = getPanelBuilder([dashboardView], [response]); - panelBuilder.getPanelsData() - .then(function(panels) { - expect(panels[0].label).toBe('MyEntity'); - }) - .finally(done); - }); - }); -}); - -function getPanelBuilder(dashboardViews, responses) { - var q = { all: function() { return mixins.buildPromise(responses); } }; - var Configuration = function() { - return { - getViewsOfType: function() { - return dashboardViews; - } - }; - }; - var location = { search: function() { return {}; } }; - var readQueries = { - getAll: function() { - return mixins.buildPromise({ data: [] }); - }, - getFilteredReferenceData: function() { return {}; }, - getOptimizedReferencedData: function() { return {}; } - }; - var AdminDescription = { getDataStore: function() { return new DataStore(); } }; - - return new PanelBuilder(q, readQueries, Configuration, AdminDescription); -} From 09cfad6f926947f982caa6e596a7b88b05578560 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Thu, 18 Jun 2015 14:08:34 +0200 Subject: [PATCH 06/10] Simplify dashboard configuration --- doc/Dashboard.md | 91 +++++++++++++------ examples/blog/config.js | 78 +++++++++++++--- package.json | 2 +- .../Crud/fieldView/WysiwygFieldView.js | 2 +- .../component/directive/maDashboardPanel.js | 16 +++- .../ng-admin/Main/view/dashboard-panel.html | 10 -- src/sass/ng-admin.scss | 26 +++++- 7 files changed, 165 insertions(+), 60 deletions(-) delete mode 100644 src/javascripts/ng-admin/Main/view/dashboard-panel.html diff --git a/doc/Dashboard.md b/doc/Dashboard.md index df1d7db2..cc90e1a3 100644 --- a/doc/Dashboard.md +++ b/doc/Dashboard.md @@ -14,47 +14,74 @@ If the default Dashboard doesn't suit your needs, create a custom dashboard conf ```js admin.dashboard(nga.dashboard() - .addCollection(name, collection) - .addCollection(name, collection) - .addCollection(name, collection) + .addCollection(collection) + .addCollection(collection) + .addCollection(collection) ); ``` -The dashboard configuration defines the dashboard datasources, using named `Collection` definitions. Collection names are useful when you use a custom dashboard template (see below). To create a collection, simply use `nga.collection()`. +The dashboard configuration defines the dashboard datasources, using `Collection` definitions. To create a collection for an entity, simply call `nga.collection(entity)`. -On each collection, you can define the same type of settings as in a `listView`: title, fields, numer of rows (`perPage()`), sorting order (`sortField()` and `sortDir()`). In addition, you can customize the position of the collection in the dashboard using the `order()` function: +On each collection, you can define the same type of settings as in a `listView`: title, fields, number of rows (`perPage()`), sorting order (`sortField()` and `sortDir()`), and list actions. In addition, you can customize the position of the collection in the dashboard using the `order()` function: ```js admin.dashboard(nga.dashboard() - .addCollection('posts', nga.collection(post) + .addCollection(nga.collection(post) + .name('recent_posts') .title('Recent posts') .perPage(5) // limit the panel to the 5 latest posts .fields([ - nga.field('title').isDetailLink(true).map(truncate) + nga.field('published_at', 'date').label('Published').format('MMM d'), + nga.field('title').isDetailLink(true).map(truncate), + nga.field('views', 'number') ]) - .sortField('id') - .sortOrder('DESC') + .sortField('published_at') + .sortDir('DESC') .order(1) ) - .addCollection('comments', nga.collection(comment) + .addCollection(nga.collection(post) + .name('popular_posts') + .title('Popular posts') + .perPage(5) // limit the panel to the 5 latest posts + .fields([ + nga.field('published_at', 'date').label('Published').format('MMM d'), + nga.field('title').isDetailLink(true).map(truncate), + nga.field('views', 'number') + ]) + .sortField('views') + .sortDir('DESC') + .order(3) + ) + .addCollection(nga.collection(comment) .title('Last comments') - .perPage(5) + .perPage(10) .fields([ - nga.field('id'), - nga.field('body', 'wysiwyg').label('Comment').stripTags(true).map(truncate), - nga.field(null, 'template').label('').template('') // you can use custom directives, too + nga.field('created_at', 'date') + .label('Posted'), + nga.field('body', 'wysiwyg') + .label('Comment') + .stripTags(true) + .map(truncate) + .isDetailLink(true), + nga.field('post_id', 'reference') + .label('Post') + .map(truncate) + .targetEntity(post) + .targetField(nga.field('title').map(truncate)) ]) + .sortField('created_at') + .sortDir('DESC') .order(2) ) - .addCollection('tags', nga.collection(tag) - .title('Recent tags') + .addCollection(nga.collection(tag) + .title('Tags publication status') .perPage(10) .fields([ - nga.field('id'), nga.field('name'), nga.field('published', 'boolean').label('Is published ?') ]) - .order(3) + .listActions(['show']) + .order(4) ) ); ``` @@ -63,11 +90,11 @@ You can add any number of collections you want, even several collections per ent As soon as you define a custom dashboard configuration, it will replace the default dashboard based on all entities and listViews. -If you just need to remove one panel from the default dasboard layout, you have to write a custom dashboard configuration from the ground up. +If you just need to remove one panel from the default dashboard layout, you have to write a custom dashboard configuration from the ground up. ## Dashboard Template -The default dashboard template iterates over all the collections, and displayes them from left to righ and from top to bottom. If you want to change the size of a particular dashboard panel, or if you want to add panels other than a datagrid, you have to write the dashboard template and set it in the dashboard: +The default dashboard template iterates over all the collections, and displays them from left to right and from top to bottom. If you want to change the size of a particular dashboard panel, or if you want to add panels other than a datagrid, you have to write the dashboard template and set it in the dashboard: ```js admin.dashboard(nga.dashboard() @@ -100,21 +127,33 @@ Here is a copy of the default dashboard template, which you can use as a startin
``` -If you decide to use a custom template, you should probably remove the `ng-repeat` and insert the collections one by one, using their names. This way, you can achieve a completely custom layout, and even include custom directives like charts. In that case, don't bother to define `title()` and `order()` in the collection, since you can more easily write them down in the template. +If you decide to use a custom template, you should probably remove the `ng-repeat` directive, and insert the collections one by one, using their names. This way, you can achieve a completely custom layout, and even include custom directives like charts. In that case, don't bother to define `title()` and `order()` in the collection, since you can more easily write them down in the template. + +For instance, here is how you can setup a dashboard with a full-width panel for comments, followed by half-width panels for posts and tags: ```html
-
+
- +
+
+
-
- +
+ +
+
+ +
+
+
+
+
- ``` +*Tip*: Collections are named after their entity. If you defined more than one collection for a given entity, override the collection name using `collection.name(customName)` in the configuration, so that you can refer to that collection name in the template. diff --git a/examples/blog/config.js b/examples/blog/config.js index 9d793ef1..562000d6 100644 --- a/examples/blog/config.js +++ b/examples/blog/config.js @@ -253,40 +253,92 @@ ); // customize dashboard + var customDashboardTemplate = + '
' + + '
' + + 'Welcome to the demo! Fell free to explore and modify the data. We reset it every few minutes.' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
'; admin.dashboard(nga.dashboard() - .addCollection('posts', nga.collection(post) + .addCollection(nga.collection(post) + .name('recent_posts') .title('Recent posts') .perPage(5) // limit the panel to the 5 latest posts .fields([ - nga.field('title').isDetailLink(true).map(truncate) + nga.field('published_at', 'date').label('Published').format('MMM d'), + nga.field('title').isDetailLink(true).map(truncate), + nga.field('views', 'number') ]) + .sortField('published_at') + .sortDir('DESC') .order(1) ) - .addCollection('comments', nga.collection(comment) + .addCollection(nga.collection(post) + .name('popular_posts') + .title('Popular posts') + .perPage(5) // limit the panel to the 5 latest posts + .fields([ + nga.field('published_at', 'date').label('Published').format('MMM d'), + nga.field('title').isDetailLink(true).map(truncate), + nga.field('views', 'number') + ]) + .sortField('views') + .sortDir('DESC') + .order(3) + ) + .addCollection(nga.collection(comment) .title('Last comments') - .perPage(5) + .perPage(10) .fields([ - nga.field('id'), + nga.field('created_at', 'date') + .label('Posted'), nga.field('body', 'wysiwyg') .label('Comment') .stripTags(true) - .map(truncate), - nga.field(null, 'template') // template fields don't need a name in dashboard view - .label('') - .template('') // you can use custom directives, too + .map(truncate) + .isDetailLink(true), + nga.field('post_id', 'reference') + .label('Post') + .map(truncate) + .targetEntity(post) + .targetField(nga.field('title').map(truncate)) ]) + .sortField('created_at') + .sortDir('DESC') .order(2) ) - .addCollection('tags', nga.collection(tag) - .title('Recent tags') + .addCollection(nga.collection(tag) + .title('Tags publication status') .perPage(10) .fields([ - nga.field('id'), nga.field('name'), nga.field('published', 'boolean').label('Is published ?') ]) - .order(3) + .listActions(['show']) + .order(4) ) + .template(customDashboardTemplate) ); nga.configure(admin); diff --git a/package.json b/package.json index 0caf3a1b..9e1f3827 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ng-admin", - "version": "0.7.0", + "version": "0.7.1", "private": false, "main": "build/ng-admin.min.js", "repository": { diff --git a/src/javascripts/ng-admin/Crud/fieldView/WysiwygFieldView.js b/src/javascripts/ng-admin/Crud/fieldView/WysiwygFieldView.js index fec813cf..952379a3 100644 --- a/src/javascripts/ng-admin/Crud/fieldView/WysiwygFieldView.js +++ b/src/javascripts/ng-admin/Crud/fieldView/WysiwygFieldView.js @@ -2,7 +2,7 @@ function getReadWidget() { return ''; } function getLinkWidget() { - return 'error: cannot display wysiwyg field as linkable'; + return '' + getReadWidget() + ''; } function getFilterWidget() { return ''; diff --git a/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js b/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js index 6e1bc012..1844905f 100644 --- a/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js +++ b/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js @@ -1,5 +1,3 @@ -var dashboardPanelView = require('../../view/dashboard-panel.html'); - function maDashboardPanel($state) { return { restrict: 'E', @@ -8,12 +6,20 @@ function maDashboardPanel($state) { entries: '&' }, link: function(scope) { - scope.collection = scope.collection(); scope.gotoList = function () { - $state.go($state.get('list'), { entity: scope.collection.entity.name() }); + $state.go($state.get('list'), { entity: scope.collection().entity.name() }); }; }, - template: dashboardPanelView + template: + '' + + '' + + '' }; } diff --git a/src/javascripts/ng-admin/Main/view/dashboard-panel.html b/src/javascripts/ng-admin/Main/view/dashboard-panel.html deleted file mode 100644 index d980ddc8..00000000 --- a/src/javascripts/ng-admin/Main/view/dashboard-panel.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/src/sass/ng-admin.scss b/src/sass/ng-admin.scss index 8b5b7938..2af76cf8 100644 --- a/src/sass/ng-admin.scss +++ b/src/sass/ng-admin.scss @@ -43,18 +43,36 @@ ul.collapsible { /** * Dashboard */ +.dashboard-starter { + margin-bottom: 30px; +} + .dashboard-content { - .panel-default { + .panel { + table { + margin-bottom: 0; + } + } - /* sb-admin uses the > selector, angular forces us to repeat the definition */ + .panel-default { .panel-heading { color: #333; background-color: #f5f5f5; border-color: #ddd; } + } - table { - margin-bottom: 0; + .panel-red a, .panel-green a, .panel-yellow a { + color: #337AB7; + } + + .panel-red a.btn-default, .panel-green a.btn-default, .panel-yellow a.btn-default { + color: #333; + } + + .panel-red, .panel-green, .panel-yellow { + .panel-heading a { + color: #fff; } } From 45e421f9f843623d5a626890d6163ee05ed41831 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Thu, 18 Jun 2015 15:36:55 +0200 Subject: [PATCH 07/10] push admin-config version requirement --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9e1f3827..dfdb52c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ng-admin", - "version": "0.7.1", + "version": "0.7.0", "private": false, "main": "build/ng-admin.min.js", "repository": { @@ -13,7 +13,7 @@ "npm": "^2.10.0" }, "devDependencies": { - "admin-config": "^0.2.0", + "admin-config": "^0.2.1", "angular": "~1.3.15", "angular-bootstrap": "^0.12.0", "angular-mocks": "^1.3.15", From 204b5da5188c4975cd3593071f038328bb99556c Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Fri, 19 Jun 2015 17:39:39 +0200 Subject: [PATCH 08/10] Fix tests --- src/javascripts/test/e2e/DashboardSpec.js | 9 +++++---- .../directive/maDashboardPanelSpec.js | 19 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/javascripts/test/e2e/DashboardSpec.js b/src/javascripts/test/e2e/DashboardSpec.js index b3ffdabb..d0c64e97 100644 --- a/src/javascripts/test/e2e/DashboardSpec.js +++ b/src/javascripts/test/e2e/DashboardSpec.js @@ -22,11 +22,12 @@ describe('Dashboard', function () { it('should display a panel for each entity with a list of recent items', function () { $$('.panel').then(function (panels) { - expect(panels.length).toBe(3); + expect(panels.length).toBe(4); - expect(panels[0].all(by.css('.panel-heading')).first().getText()).toBe('Recent posts'); - expect(panels[1].all(by.css('.panel-heading')).first().getText()).toBe('Recent tags'); - expect(panels[2].all(by.css('.panel-heading')).first().getText()).toBe('Last comments'); + expect(panels[0].all(by.css('.panel-heading')).first().getText()).toBe('Last comments'); + expect(panels[1].all(by.css('.panel-heading')).first().getText()).toBe('Recent posts'); + expect(panels[2].all(by.css('.panel-heading')).first().getText()).toBe('Popular posts'); + expect(panels[3].all(by.css('.panel-heading')).first().getText()).toBe('Tags publication status'); }); }); }); diff --git a/src/javascripts/test/unit/Main/component/directive/maDashboardPanelSpec.js b/src/javascripts/test/unit/Main/component/directive/maDashboardPanelSpec.js index 39617429..60be1aeb 100644 --- a/src/javascripts/test/unit/Main/component/directive/maDashboardPanelSpec.js +++ b/src/javascripts/test/unit/Main/component/directive/maDashboardPanelSpec.js @@ -6,8 +6,7 @@ describe('directive: ma-dashboard-panel', function () { Entity = require('admin-config/lib/Entity/Entity'), $compile, scope, - directiveUsage = ''; + directiveUsage = ''; angular.module('testapp_state', []) .service('$state', function($q) { @@ -43,18 +42,18 @@ describe('directive: ma-dashboard-panel', function () { beforeEach(inject(function (_$compile_, _$rootScope_) { $compile = _$compile_; scope = _$rootScope_; - scope.label = ''; - scope.viewName = ''; - scope.fields = []; - scope.entries = []; - scope.entity = new Entity(); - scope.perPage = 15; })); it("should display a title with a datagrid", function () { + scope.collection = { + title: () => 'Comments', + name: () => 'myView', + fields: () => [], + entity: new Entity(), + listActions: () => {} + }; + scope.entries = []; var element = $compile(directiveUsage)(scope); - scope.label = 'Comments'; - scope.viewName = 'myView'; scope.$digest(); expect(element[0].querySelector('.panel-heading').innerHTML).toContain('Comments'); From c9b84c8b7d7ab637d1a42c3b966475afe980fdad Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Fri, 19 Jun 2015 17:43:08 +0200 Subject: [PATCH 09/10] Code review --- UPGRADE-0.8.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE-0.8.md b/UPGRADE-0.8.md index ab0866cc..658ae2bd 100644 --- a/UPGRADE-0.8.md +++ b/UPGRADE-0.8.md @@ -22,7 +22,7 @@ The `Field.identifier(true)` does not exist anymore. Instead, you must specify t ## `dashboardView()` is deprecated, use `admin.dashboard()` instead -To let developers customize the admin dashboard at will, ng-admin 0.8 decouples the dashboard data and presentation. You can setup the dashboard datasources and template on a new member of ythe admin class, `dahboard()`: +To let developers customize the admin dashboard at will, ng-admin 0.8 decouples the dashboard data and presentation. You can setup the dashboard datasources and template on a new member of the admin class, `dashboard()`: ```js admin.dashboard(nga.dashboard() From a4187de0cd7c54363cc6225f72fe9093b8a8b4c8 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Fri, 19 Jun 2015 18:00:04 +0200 Subject: [PATCH 10/10] Fix dashboard panel title when dashboard is undefined --- .../ng-admin/Main/component/directive/maDashboardPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js b/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js index 1844905f..57f02073 100644 --- a/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js +++ b/src/javascripts/ng-admin/Main/component/directive/maDashboardPanel.js @@ -12,7 +12,7 @@ function maDashboardPanel($state) { }, template: '' + '