├── README.md ├── admin.js ├── client ├── autoform.js ├── breadcrumb │ ├── breadcrumb.html │ └── breadcrumb.js ├── collections │ ├── collections.html │ └── collections.js ├── common │ ├── login.html │ ├── login.js │ └── tree.js ├── dashboard │ ├── dashboard.html │ └── dashboard.js ├── defaults.js ├── header │ └── header.html ├── helpers.js ├── layout │ ├── layout.html │ └── layout.js ├── navbar │ ├── navbar.html │ └── navbar.js ├── sidebar │ ├── sidebar.html │ └── sidebar.js └── style.css ├── lib ├── admin.js ├── collections.js └── router.js └── package.js /README.md: -------------------------------------------------------------------------------- 1 | Meteor Admin 2 | ============ 3 | 4 | `$ meteor add mfactory:admin` 5 | 6 | This is the next version of `yogiben:admin`. It features a lot of improvements in the api and AdminLTE theme. Please bear in mind that this is one of the first releases so the api is not set in stone and it also may contain some bugs. 7 | 8 | ## Admin config ## 9 | 10 | Admin package can be configured through `Admin.config` variable. It's `ReactiveDict`, so values can be set and get reactively, e.g: 11 | 12 | ```javascript 13 | Admin.config.set('name', 'My Admin Page'); 14 | Admin.config.get('name'); // "My Admin Page" 15 | ``` 16 | 17 | Config can be accessed in Blaze templates through `Admin.config` helper, e.g: 18 | 19 | ```html 20 |

21 | {{Admin.config.get 'name'}} 22 |

23 | ``` 24 | 25 | ### Available options ### 26 | 27 | - **name** - *string* 28 | 29 | Name of admin dashboard. Will be displayed in the navbar. Defaults to `Admin`. 30 | 31 | - **layoutTemplate** - *string* 32 | 33 | Name of the layout template. Defaults to `mfAdminLayout`. 34 | 35 | ## Admin permissions ## 36 | 37 | All admin routes and publications check if logged user has admin permissions with `Admin.isAdmin` function. By any user with `admin` role (see [alanning:roles](https://github.com/alanning/meteor-roles) package) has admin permissions. You can overwrite this function e.g: 38 | 39 | ```javascript 40 | Admin.isAdmin = function (userId) { 41 | return userId === SUPERUSERID; 42 | }; 43 | ``` 44 | 45 | ## Sidebar and navbar menus ## 46 | 47 | Sidebar and navbar menus are designed to work well with your app as well as with other packages. For example `admin-analytics` package may add `Analytics` link to the sidebar: 48 | 49 | ```javascript 50 | Admin.sidebar.set('AdminAnalytics', { 51 | label: 'Analytics', 52 | icon: 'area-chart', 53 | path: '/analytics' 54 | }); 55 | ``` 56 | 57 | The first argument is unique key. This key can be used to alter the sidebar item. Suppose you want to change the icon. This can be done like this: 58 | 59 | ```javascript 60 | Admin.sidebar.set('AdminAnalytics', { 61 | icon: 'pie-chart' 62 | }); 63 | ``` 64 | 65 | Sidebar items may have many children. For example if you want to add subitem to `AdminAnalytics` from previous snippet you have to create another item which key is parent key followed by dot and your item key, e.g: 66 | 67 | ```javascript 68 | Admin.sidebar.set('AdminAnalytics.Users', { 69 | label: 'User stats', 70 | path: '/analytics/users' 71 | }); 72 | ``` 73 | 74 | Children items can be changed the same way, e.g: 75 | 76 | ```javascript 77 | Admin.sidebar.set('AdminAnalytics.Users', { 78 | icon: 'user' 79 | }); 80 | ``` 81 | 82 | This will add an icon next to the label. 83 | 84 | API for the navbar menu is same as for sidebar. For example: 85 | 86 | ```javascript 87 | Admin.navbar.set('Profile', { 88 | path: '/profile' 89 | }); 90 | ``` 91 | 92 | ### Arguments ### 93 | 94 | `Admin.sidebar.set(key, options)` 95 | 96 | or 97 | 98 | `Admin.navbar.set(key, options)` 99 | 100 | - **key** - *string* 101 | 102 | The unique key of the item. Parent items are separated from children by dot. 103 | 104 | It's recomended for packages to use package name as a key, e.g. `foo:analytics.Users`. Or if many root items are needed: `foo:analytics/Analytics` `foo:analytics/Statistics`. This convention helps to avoid name clash. 105 | 106 | - **options** - *object* 107 | 108 | Object containing item options like label, icon etc. See below for all possible values: 109 | 110 | - **label** - *string* 111 | 112 | Text of the link. 113 | 114 | - **icon** - *string* 115 | 116 | Name of the icon to be displayed next to the label. It should be font awesome icon without `fa-` prefix. You can check all available icons [here](http://fortawesome.github.io/Font-Awesome/icons/). 117 | 118 | - **hidden** - *boolean* 119 | 120 | If `true` the item (and his children if any) won't be displayed in the sidebar. By default it's `false`. 121 | 122 | - **path** - *string* 123 | 124 | Admin path to be used as the url of the item link. Admin path will be added to your path automatically. So if your path is `/analytics` and admin path is `/admin` the result will be '/admin/analytics'. 125 | 126 | - **url** - *string* 127 | 128 | You can use this property instead of `path` if you want to link to outside of admin dashboard. This won't be modified so you can use as a path to your app, e.g. `/home` or to other app, e.g. `https://facebook.com`. 129 | 130 | - **order** - *number* 131 | 132 | Allows you to control the order of items in the sidebar. All sidebar items are sorted descending by this number. Defaults to 0. 133 | 134 | - **template** - *string* 135 | 136 | Name of the template to be used instead of default one. For example you can use custom template to add a badge with a number of posts. 137 | 138 | ## Collections ## 139 | 140 | Admin allows you to easily add CRUD views for your collections to the dashboard. Collection must have defined schema with `aldeed:simple-schema` package. 141 | 142 | ### CRUD views ### 143 | 144 | There are 3 views that will be generated: 145 | 146 | - **View all documents** 147 | 148 | Displays table with all documents in the collection. Uses using `aldeed:tabular package`. 149 | 150 | - **Edit document** 151 | 152 | Displays an update form. Uses `aldeed:autoform` package. 153 | 154 | - **New document** 155 | 156 | Displays an insert form. Uses `aldeed:autoform` package. 157 | 158 | - **Delete document** 159 | 160 | Displays confirmation modal when delete button is clicked. 161 | 162 | ### Adding a collection ### 163 | 164 | `Admin.collections.add(name, options)` 165 | 166 | Assume we have this collection of posts: 167 | 168 | ```javascript 169 | Posts = new Mongo.Collection('posts'); 170 | 171 | Posts.attachSchema( 172 | new SimpleSchema({ 173 | title: { 174 | type: String, 175 | max: 80 176 | }, 177 | 178 | content: { 179 | type: String, 180 | autoform: { 181 | afFieldInput: { 182 | type: 'textarea', 183 | rows: 4 184 | } 185 | } 186 | }, 187 | 188 | owner: { 189 | type: String, 190 | regEx: SimpleSchema.RegEx.Id 191 | } 192 | }) 193 | ); 194 | ``` 195 | 196 | Next we can add this collection to our dashboard like this: 197 | 198 | ```javascript 199 | Admin.collections.add('Posts'); 200 | ``` 201 | 202 | You should notice new sidebar item: **Posts** with **New** and **View all** subitems. 203 | 204 | ### Available options ### 205 | 206 | - **collection** - *string* 207 | 208 | Name of collection object defined in global namespace. Can contain dots (e.g. `"MyCollections.Posts"`). Defaults to `name` argument of `Admin.collections.add`. 209 | 210 | - **columns** - *array* 211 | 212 | Custom columns to be displayed in table view. See [aldeed:tabular](https://github.com/aldeed/meteor-tabular) docs to learn how to define them. 213 | 214 | - **icon** - *string* 215 | 216 | Name of fontawesome icon (without `fa-` prefix) to be displayed in the sidebar. 217 | 218 | - **extraFields** - *array* 219 | 220 | Will be passed to `Tabular.Table` constructor options. See [aldeed:tabular](https://github.com/aldeed/meteor-tabular) docs for more info. 221 | 222 | - **changeSelector** - *function* 223 | 224 | Will be passed to `Tabular.Table` constructor options. See [aldeed:tabular](https://github.com/aldeed/meteor-tabular) docs for more info. 225 | -------------------------------------------------------------------------------- /admin.js: -------------------------------------------------------------------------------- 1 | // Write your package code here! 2 | -------------------------------------------------------------------------------- /client/autoform.js: -------------------------------------------------------------------------------- 1 | AutoForm.hooks({ 2 | mfAdmin_insert: { 3 | onSuccess: function (formType, collection) { 4 | Admin.go(this.template.data._successPath); 5 | } 6 | }, 7 | 8 | mfAdmin_update: { 9 | onSuccess: function (formType, collection) { 10 | Admin.go(this.template.data._successPath); 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /client/breadcrumb/breadcrumb.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /client/breadcrumb/breadcrumb.js: -------------------------------------------------------------------------------- 1 | Template.mfAdminBreadcrumb.helpers({ 2 | breadcrumb: function () { 3 | return _.map(this.breadcrumb, function (item, index, arr) { 4 | var isActive = index === (arr.length - 1); 5 | return _.extend({}, 6 | item, 7 | isActive ? { isActive: isActive, path: null, url: null } : {}); 8 | }); 9 | }, 10 | 11 | url: function () { 12 | if (this.path) { 13 | return Admin.path(this.path); 14 | } 15 | 16 | if (this.url) { 17 | return this.url; 18 | } 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /client/collections/collections.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | 33 | 46 | 47 | 69 | 70 | 74 | 75 | 79 | 80 | 99 | -------------------------------------------------------------------------------- /client/collections/collections.js: -------------------------------------------------------------------------------- 1 | var adminDeleteModalCallback; 2 | 3 | Admin.dashboard.set('collectionWidgets', { 4 | template: 'mfAdminCollectionsWidgets' 5 | }); 6 | 7 | Template.mfAdminCollectionsWidgets.onCreated(function () { 8 | var self = this; 9 | _.each(Admin.collections.get(), function (collection) { 10 | self.subscribe(collection.countPubName) 11 | }); 12 | }); 13 | 14 | Template.mfAdminCollectionsWidgets.helpers({ 15 | background: function () { 16 | return (this.widget && this.widget.color) || 'blue'; 17 | } 18 | }); 19 | 20 | Template.mfAdminCollectionsView.onCreated(function () { 21 | this.subscribe(this.data.countPubName); 22 | }); 23 | 24 | Template.mfAdminCollectionsView.helpers({ 25 | hasDocuments: function () { 26 | return Counts.get(this.countPubName) > 0; 27 | } 28 | }); 29 | 30 | Template.mfAdminCollectionsView.events({ 31 | // This callback should be attached to mfAdminCollectionsDeleteBtn template 32 | // but for some reason templates rendered with Blaze.renderWithData are not 33 | // firing event callbacks 34 | 'click .js-delete-doc': function (e, t) { 35 | var collection = this.table.collection; 36 | var _id = $(e.currentTarget).attr('data-id'); 37 | 38 | adminDeleteModalCallback = function () { 39 | collection.remove(_id); 40 | }; 41 | 42 | $('#admin-delete-modal').modal('show'); 43 | } 44 | }); 45 | 46 | Template.mfAdminCollectionsDeleteModal.events({ 47 | 'click .js-delete': function (e, t) { 48 | if (typeof adminDeleteModalCallback === 'function') { 49 | adminDeleteModalCallback(); 50 | t.$('#admin-delete-modal').modal('hide'); 51 | } 52 | }, 53 | 54 | 'hidden.bs.modal #admin-delete-modal': function (e, t) { 55 | adminDeleteModalCallback = null; 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /client/common/login.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /client/common/login.js: -------------------------------------------------------------------------------- 1 | Template.mfAdminLogin.helpers({ 2 | serviceTemplate: function () { 3 | var loginLayoutTemplate = Admin.config.get('loginLayoutTemplate'); 4 | 5 | if (loginLayoutTemplate){ 6 | return loginLayoutTemplate; 7 | } 8 | 9 | if (Package['useraccounts:bootstrap']) { 10 | return 'mfAdminLoginUseraccountsBootstrap' 11 | }; 12 | 13 | if (Package['accounts-ui']) { 14 | return 'mfAdminLoginAccountsUI'; 15 | } 16 | 17 | throw new Error('Missing template for login page.'); 18 | }, 19 | 20 | data: function () { 21 | return { 22 | redirectPath: Admin.path() 23 | }; 24 | } 25 | }); 26 | 27 | Template.mfAdminLogin.onCreated(function () { 28 | this.autorun(function () { 29 | var user = Meteor.user(); 30 | if (Admin.isAdmin(Meteor.userId())) { 31 | Admin.go('/'); 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /client/common/tree.js: -------------------------------------------------------------------------------- 1 | Tree = function () { 2 | this._tree = {}; 3 | this._dep = new Tracker.Dependency(); 4 | }; 5 | 6 | Tree.prototype.set = function (path, options) { 7 | this._dep.changed(); 8 | 9 | _.reduce(path.split('.'), function (tree, key, index, path) { 10 | var isLeaf = index === (path.length - 1); 11 | 12 | if (! tree[key]) { 13 | tree[key] = { 14 | label: key, 15 | _tree: {} 16 | }; 17 | } 18 | 19 | if (isLeaf) { 20 | _.extend(tree[key], options); 21 | } 22 | 23 | return tree[key]._tree; 24 | }, this._tree); 25 | }; 26 | 27 | Tree.prototype.get = function (path) { 28 | this._dep.depend(); 29 | 30 | if (! path) { 31 | return this._tree; 32 | } 33 | 34 | return _.reduce(path.split('.'), function (tree, key, index, path) { 35 | var isLeaf = index === (path.length - 1); 36 | return isLeaf ? tree[key] : tree[key]._tree; 37 | }, this._tree); 38 | }; 39 | -------------------------------------------------------------------------------- /client/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /client/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | var DashboardImpl = function () { 2 | this._items = {}; 3 | this._dep = new Tracker.Dependency(); 4 | }; 5 | 6 | DashboardImpl.prototype.set = function (key, value) { 7 | this._items[key] = value; 8 | this._dep.changed(); 9 | }; 10 | 11 | DashboardImpl.prototype.getSorted = function () { 12 | this._dep.depend(); 13 | return _.sortBy(_.values(this._items), function (item) { 14 | return item.order || 0; 15 | }); 16 | }; 17 | 18 | Admin.dashboard = new DashboardImpl(); 19 | 20 | Template.mfAdminDashboard.helpers({ 21 | items: function () { 22 | return Admin.dashboard.getSorted(); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /client/defaults.js: -------------------------------------------------------------------------------- 1 | Admin.navbar.set('Home', { 2 | url: '/' 3 | }); 4 | 5 | Admin.sidebar.set('Dashboard', { 6 | path: '/', 7 | icon: 'tachometer', 8 | order: 100 9 | }); 10 | -------------------------------------------------------------------------------- /client/header/header.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /client/helpers.js: -------------------------------------------------------------------------------- 1 | Template.registerHelper('Admin', Admin); 2 | -------------------------------------------------------------------------------- /client/layout/layout.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /client/layout/layout.js: -------------------------------------------------------------------------------- 1 | Template.mfAdminLayout.onCreated(function () { 2 | var self = this; 3 | self.minHeight = new ReactiveVar(); 4 | self.resize = function () { 5 | self.minHeight.set( 6 | $(window).height() - ($('.navbar').height() || 0)); 7 | }; 8 | 9 | $(window).bind('resize', self.resize); 10 | }); 11 | 12 | Template.mfAdminLayout.onRendered(function () { 13 | this.resize(); 14 | }); 15 | 16 | Template.mfAdminLayout.onDestroyed(function () { 17 | $(window).unbind('resize', this.resize); 18 | }); 19 | 20 | Template.mfAdminLayout.helpers({ 21 | AdminLTE_skin: function () { 22 | return Admin.config.get('admin-lte-skin'); 23 | }, 24 | 25 | AdminLTE_fixed: function () { 26 | return Admin.config.get('admin-lte-fixed'); 27 | }, 28 | 29 | AdminLTE_sidebarMini: function () { 30 | return Admin.config.get('admin-lte-sidebar-mini'); 31 | }, 32 | 33 | minHeight: function () { 34 | return Template.instance().minHeight.get() + 'px'; 35 | }, 36 | 37 | contentData: function () { 38 | var routeOptions = Admin._currentRouteOptions(); 39 | return routeOptions && routeOptions.data && 40 | routeOptions.data(Admin._routeParams()); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /client/navbar/navbar.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | 29 | 38 | 39 | 53 | -------------------------------------------------------------------------------- /client/navbar/navbar.js: -------------------------------------------------------------------------------- 1 | Admin.navbar = new Tree(); 2 | 3 | var navbarHelpers = { 4 | items: function (ctx) { 5 | var tree = ctx && ctx._tree ? ctx._tree : Admin.navbar.get(); 6 | return _.sortBy(_.values(tree), function (item) { 7 | return item && item.order ? item.order : 0; 8 | }).reverse(); 9 | }, 10 | 11 | hasSubItems: function (ctx) { 12 | return ctx && _.keys(ctx._tree).length > 0; 13 | }, 14 | 15 | href: function () { 16 | if (this.url) { 17 | return this.url; 18 | } 19 | 20 | if (this.path) { 21 | return Admin.path(this.path); 22 | } 23 | 24 | return '#'; 25 | } 26 | }; 27 | 28 | Template.mfAdminNavbarItems.helpers(navbarHelpers); 29 | Template.mfAdminNavbarItem.helpers(navbarHelpers); 30 | Template.mfAdminNavbarDropdown.helpers(navbarHelpers); 31 | -------------------------------------------------------------------------------- /client/sidebar/sidebar.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 37 | 38 | 56 | -------------------------------------------------------------------------------- /client/sidebar/sidebar.js: -------------------------------------------------------------------------------- 1 | Admin.sidebar = new Tree(); 2 | 3 | var sidebarHelpers = { 4 | items: function (ctx) { 5 | var tree = ctx && ctx._tree ? ctx._tree : Admin.sidebar.get(); 6 | return _.sortBy(_.values(tree), function (item) { 7 | return item && item.order ? item.order : 0; 8 | }).reverse(); 9 | }, 10 | 11 | hasSubItems: function (ctx) { 12 | return ctx && _.keys(ctx._tree).length > 0; 13 | }, 14 | 15 | href: function () { 16 | if (this.url) { 17 | return this.url; 18 | } 19 | 20 | if (this.path) { 21 | return Admin.path(this.path); 22 | } 23 | 24 | return '#'; 25 | } 26 | }; 27 | 28 | Template.mfAdminSidebarItems.helpers(sidebarHelpers); 29 | Template.mfAdminSidebarItem.helpers(sidebarHelpers); 30 | Template.mfAdminSidebarTreeview.helpers(sidebarHelpers); 31 | -------------------------------------------------------------------------------- /client/style.css: -------------------------------------------------------------------------------- 1 | 2 | th.admin-sortable 3 | { 4 | cursor: pointer; 5 | } 6 | 7 | .header 8 | { 9 | padding:0; 10 | } 11 | 12 | .dataTables_wrapper table.dataTable { 13 | width: 100% !important; 14 | } 15 | 16 | .dataTables_wrapper table.dataTable > tbody > tr > td { 17 | background-color: white; 18 | } 19 | 20 | .dataTables_wrapper table.dataTable > thead:first-child > tr:first-child > th, 21 | .dataTables_wrapper table.dataTable > tbody > tr:nth-child(even) > td { 22 | background-color: #f3f4f5 !important; 23 | } 24 | 25 | .dataTables_wrapper table.dataTable > thead:first-child > tr:first-child > th { 26 | border-bottom: none !important; 27 | padding: 8px 28 | } 29 | 30 | .dataTables_wrapper table.dataTable > thead > tr:first-child > th, 31 | .dataTables_wrapper table.dataTable > tbody > tr > td { 32 | border-top: 1px solid #ddd !important; 33 | } 34 | 35 | .dataTables_wrapper .box-body { 36 | padding: 0; 37 | } 38 | 39 | .dataTables_wrapper .box > .box-header { 40 | padding-bottom: 0; 41 | } 42 | 43 | .dataTables_wrapper .box-toolbar { 44 | padding: 10px; 45 | } 46 | 47 | .dataTables_wrapper .box-toolbar .pull-left { 48 | min-width: 310px; 49 | } 50 | 51 | .dataTables_wrapper .box-toolbar .dataTables_filter, 52 | .dataTables_wrapper .box-toolbar .dataTables_length { 53 | float: left; 54 | } 55 | 56 | .dataTables_wrapper .box-toolbar .dataTables_length { 57 | margin-right: 8px; 58 | } 59 | 60 | .dataTables_wrapper .box-toolbar .dataTables_filter input { 61 | margin: 0; 62 | } 63 | -------------------------------------------------------------------------------- /lib/admin.js: -------------------------------------------------------------------------------- 1 | Admin = { 2 | _createRouteCallbacks: [], 3 | _routeOptions: {}, 4 | config: new ReactiveDict() 5 | }; 6 | 7 | Admin.config.setDefault('loginLayoutTemplate', undefined); 8 | Admin.config.setDefault('layoutTemplate', 'mfAdminLayout'); 9 | Admin.config.setDefault('name', 'Admin'); 10 | 11 | Admin.isAdmin = function (userId) { 12 | if (Package['alanning:roles']) { 13 | return Roles.userIsInRole(userId, ['admin']); 14 | } 15 | 16 | return false; 17 | }; 18 | 19 | Admin.path = function (path) { 20 | path = path || ''; 21 | if (path[0] === '/') { 22 | return '/admin' + path; 23 | } 24 | return '/admin/' + path; 25 | }; 26 | 27 | Admin.go = function (path) { 28 | var IronRouter = Package['iron:router'] && Package['iron:router'].Router; 29 | var FlowRouter = Package['kadira:flow-router'] && 30 | Package['kadira:flow-router'].FlowRouter; 31 | if (IronRouter) 32 | return IronRouter.go(Admin.path(path)); 33 | if (FlowRouter) 34 | return FlowRouter.go(Admin.path(path)); 35 | }; 36 | 37 | Admin.route = function (path, options) { 38 | path = Admin.path(path); 39 | _.each(Admin._createRouteCallbacks, function (cb) { 40 | cb(path, options); 41 | }); 42 | }; 43 | 44 | Admin._currentRouteOptions = function () { 45 | if (!!Package['iron:router']) { 46 | return Admin._routeOptions[Router.current().route.getName()]; 47 | } 48 | if (!!Package['kadira:flow-router']) { 49 | return Admin._routeOptions[FlowRouter.current().route.name]; 50 | } 51 | }; 52 | 53 | Admin._routeParams = function () { 54 | if (!!Package['iron:router']) { 55 | var params = Router.current().params; 56 | return _.pick(params, _.without(_.keys(params), 'hash', 'query')); 57 | } 58 | if (!!Package['kadira:flow-router']) { 59 | return FlowRouter.current().params; 60 | } 61 | }; 62 | 63 | Admin.onRouteCreate = function (cb) { 64 | if (typeof cb === 'function') { 65 | Admin._createRouteCallbacks.push(cb); 66 | } 67 | }; 68 | 69 | Admin.onRouteCreate(function (path, options) { 70 | var IronRouter = Package['iron:router'] && Package['iron:router'].Router; 71 | var FlowRouter = Package['kadira:flow-router'] && 72 | Package['kadira:flow-router'].FlowRouter; 73 | if (!(IronRouter || FlowRouter)) 74 | throw new Error( 75 | 'mfactory:admin requires kadira:flow-router or iron:router to be installed'); 76 | 77 | 78 | var data = { 79 | content: options.template, 80 | contentHeader: options.contentHeader || 'mfAdminContentHeader', 81 | _path: path 82 | }; 83 | 84 | Admin._routeOptions[path] = options; 85 | 86 | if (IronRouter) 87 | createIronRoute(IronRouter, path, data); 88 | if (FlowRouter && Meteor.isClient) 89 | createFlowRoute(FlowRouter, path, data); 90 | }); 91 | 92 | function createIronRoute(router, path, data) { 93 | router.route(path, { 94 | name: path, 95 | layoutTemplate: data.layoutTemplate || Admin.config.get('layoutTemplate'), 96 | onBeforeAction: function () { 97 | if (!Admin.isAdmin(Meteor.userId())) 98 | return Admin.go(Admin.path('/login')); 99 | this.next(); 100 | }, 101 | data: { content: data.content } 102 | }); 103 | }; 104 | 105 | function createFlowRoute(router, path, data) { 106 | var BlazeLayout = Package['kadira:blaze-layout'].BlazeLayout; 107 | router.route(path, { 108 | name: path, 109 | action: function () { 110 | if (!Admin.isAdmin(Meteor.userId())) 111 | return Admin.go(Admin.path('/login')); 112 | BlazeLayout.render( 113 | data.layoutTemplate || Admin.config.get('layoutTemplate'), 114 | { content: data.content }); 115 | } 116 | }); 117 | } 118 | 119 | Admin.route('/login', { 120 | template: 'mfAdminLogin', 121 | layoutTemplate: 'mfAdminLoginLayout' 122 | }); 123 | -------------------------------------------------------------------------------- /lib/collections.js: -------------------------------------------------------------------------------- 1 | Admin.collections = { 2 | _collections: {}, 3 | _dep: new Tracker.Dependency() 4 | }; 5 | 6 | var lookupCollection = function (obj, root) { 7 | root = root || (Meteor.isServer ? global : window); 8 | if (typeof obj === 'string') { 9 | var ref = root; 10 | var arr = obj.split('.'); 11 | while (arr.length && (ref = ref[arr.shift()])) { 12 | continue; 13 | } 14 | if (! ref) { 15 | throw new Error(obj + ' is not in the ' + root.toString()); 16 | } 17 | return ref; 18 | } 19 | return obj; 20 | }; 21 | 22 | var getCollectionColumns = function (collection) { 23 | if (collection && collection._c2 && collection._c2._simpleSchema) { 24 | return _.map(collection._c2._simpleSchema._schemaKeys, function (key) { 25 | return { data: key, title: key }; 26 | }); 27 | } 28 | 29 | return [{ data: '_id', title: 'ID' }]; 30 | } 31 | 32 | Admin.collections.add = function (name, options) { 33 | var viewPath = '/' + name; 34 | var newPath = '/' + name + '/new'; 35 | var editPath = '/' + name + '/edit'; 36 | var countPubName = 'mfAdmin-' + name + '-count'; 37 | var collection = options.collection || lookupCollection(name); 38 | var icon = options.icon || 'plus'; 39 | var columns = options.columns || getCollectionColumns(collection); 40 | 41 | columns.push({ 42 | data: '_id', 43 | title: 'Edit', 44 | createdCell: function (node, cellData, rowData) { 45 | var _id = cellData; 46 | $(node).html( 47 | Blaze.toHTMLWithData( 48 | Template.mfAdminCollectionsEditBtn, 49 | { path: Admin.path(editPath + '/' + _id) })); 50 | }, 51 | width: '40px', 52 | orderable: false 53 | }); 54 | 55 | columns.push({ 56 | data: '_id', 57 | title: 'Delete', 58 | createdCell: function (node, cellData, rowData) { 59 | var _id = cellData; 60 | $(node).html( 61 | Blaze.toHTMLWithData( 62 | Template.mfAdminCollectionsDeleteBtn, 63 | { _id: _id })); 64 | }, 65 | width: '40px', 66 | orderable: false 67 | }); 68 | 69 | var table = new Tabular.Table({ 70 | name: 'mfAdminTables.' + name, 71 | collection: collection, 72 | columns: columns, 73 | extraFields: options.extraFields, 74 | changeSelector: options.changeSelector, 75 | allow: function (userId) { 76 | return Admin.isAdmin(userId); 77 | } 78 | }); 79 | 80 | Admin.route(viewPath, { 81 | template: 'mfAdminCollectionsView', 82 | data: function () { 83 | return { 84 | title: name, 85 | subtitle: 'View', 86 | breadcrumb: [ 87 | { 88 | label: name, 89 | path: viewPath, 90 | icon: icon 91 | } 92 | ], 93 | table: table, 94 | countPubName: countPubName, 95 | newPath: Admin.path(newPath) 96 | }; 97 | } 98 | }); 99 | 100 | Admin.route(newPath, { 101 | template: 'mfAdminCollectionsNew', 102 | data: function () { 103 | return { 104 | title: name, 105 | subtitle: 'New', 106 | breadcrumb: [ 107 | { 108 | label: name, 109 | path: viewPath, 110 | icon: icon 111 | }, 112 | { 113 | label: 'New', 114 | path: newPath 115 | } 116 | ], 117 | collection: function () { 118 | return collection; 119 | }, 120 | successPath: viewPath 121 | }; 122 | } 123 | }); 124 | 125 | Admin.route(editPath + '/:_id', { 126 | template: 'mfAdminCollectionsEdit', 127 | data: function (params) { 128 | return { 129 | title: name, 130 | subtitle: 'Edit', 131 | breadcrumb: [ 132 | { 133 | label: name, 134 | path: viewPath, 135 | icon: icon 136 | }, 137 | { 138 | label: 'Edit', 139 | path: editPath + '/' + params._id 140 | } 141 | ], 142 | collection: collection, 143 | doc: collection.findOne(params._id), 144 | successPath: viewPath 145 | }; 146 | } 147 | }); 148 | 149 | if (Meteor.isClient) { 150 | var id = 'collection-' + name; 151 | 152 | Admin.sidebar.set(id, { 153 | label: name, 154 | icon: icon 155 | }); 156 | 157 | Admin.sidebar.set(id + '.view', { 158 | label: 'View all', 159 | path: '/' + name, 160 | order: 10 161 | }); 162 | 163 | Admin.sidebar.set(id + '.create', { 164 | label: 'New', 165 | path: '/' + name + '/new', 166 | order: 20 167 | }); 168 | } 169 | 170 | if (Meteor.isServer) { 171 | Meteor.publish(countPubName, function () { 172 | Counts.publish(this, countPubName, collection.find()); 173 | }); 174 | } 175 | 176 | Admin.collections._collections[name] = { 177 | name: name, 178 | collection: collection, 179 | viewPath: Admin.path(viewPath), 180 | newPath: Admin.path(newPath), 181 | countPubName: countPubName, 182 | widget: options.widget || {} 183 | }; 184 | Admin.collections._dep.changed(); 185 | }; 186 | 187 | Admin.collections.get = function (name) { 188 | Admin.collections._dep.depend(); 189 | 190 | if (name) { 191 | return Admin.collections._collections[name]; 192 | } 193 | 194 | return _.values(Admin.collections._collections); 195 | }; 196 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | Admin.route('/', { 2 | template: 'mfAdminDashboard', 3 | data: function () { 4 | return { 5 | title: 'Dashboard' 6 | }; 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'mfactory:admin', 3 | version: '0.0.2', 4 | summary: 'A complete admin dashboard solution', 5 | git: 'https://github.com/meteor-factory/meteor-admin.git', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.1.0.2'); 11 | 12 | api.use([ 13 | 'templating', 14 | 'underscore', 15 | 'tracker', 16 | 'reactive-var', 17 | 'reactive-dict', 18 | 'mfactory:admin-lte@0.0.1', 19 | 'aldeed:autoform@5.3.0', 20 | 'aldeed:tabular@1.2.0', 21 | 'tmeasday:publish-counts@0.4.0' 22 | ]); 23 | 24 | api.use([ 25 | 'iron:router@1.0.7', 26 | 'kadira:flow-router@2.10.0', 27 | 'kadira:blaze-layout@2.3.0', 28 | 'accounts-ui', 29 | 'useraccounts:core@1.11.1', 30 | 'useraccounts:bootstrap@1.11.1' 31 | ], { weak: true }); 32 | 33 | api.addFiles([ 34 | 'lib/admin.js', 35 | 'lib/router.js', 36 | 'lib/collections.js' 37 | ]); 38 | 39 | api.addFiles([ 40 | 'client/common/tree.js', 41 | 'client/common/login.html', 42 | 'client/common/login.js', 43 | 'client/header/header.html', 44 | 'client/layout/layout.html', 45 | 'client/layout/layout.js', 46 | 'client/sidebar/sidebar.html', 47 | 'client/sidebar/sidebar.js', 48 | 'client/navbar/navbar.html', 49 | 'client/navbar/navbar.js', 50 | 'client/dashboard/dashboard.html', 51 | 'client/dashboard/dashboard.js', 52 | 'client/collections/collections.html', 53 | 'client/collections/collections.js', 54 | 'client/breadcrumb/breadcrumb.html', 55 | 'client/breadcrumb/breadcrumb.js', 56 | 'client/helpers.js', 57 | 'client/defaults.js', 58 | 'client/autoform.js', 59 | 'client/style.css' 60 | ], 'client'); 61 | 62 | api.export('Admin'); 63 | }); 64 | --------------------------------------------------------------------------------