├── .bowerrc ├── .csslintrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── config ├── README.md └── config_example.json ├── gulpfile.js ├── karma.conf.js ├── package.json ├── scsslint.yml └── src ├── app ├── admin │ ├── admin.js │ └── loginHistory │ │ ├── index.html │ │ ├── login-history-controllers.js │ │ ├── login-history-models.js │ │ └── login-history.js ├── app.js ├── app.scss ├── assets │ └── .gitkeep ├── core │ ├── auth │ │ ├── auth.js │ │ ├── login │ │ │ ├── login-controllers.js │ │ │ ├── login.html │ │ │ ├── login.js │ │ │ └── login.scss │ │ └── services │ │ │ ├── AuthService.js │ │ │ ├── UserService.js │ │ │ └── services.js │ ├── components │ │ ├── FocusOn.js │ │ └── components.js │ ├── constants │ │ ├── AccessLevels.js │ │ └── BackendConfig.js │ ├── core.js │ ├── dependencies │ │ └── dependencies.js │ ├── directives │ │ ├── ListSearch.js │ │ ├── directives.js │ │ └── partials │ │ │ └── ListSearch.html │ ├── error │ │ ├── error-controllers.js │ │ ├── error.js │ │ └── partials │ │ │ └── error.html │ ├── filters │ │ └── filters.js │ ├── interceptors │ │ ├── AuthInterceptor.js │ │ ├── ErrorInterceptor.js │ │ └── interceptors.js │ ├── layout │ │ ├── layout-controllers.js │ │ ├── layout-directives.js │ │ ├── layout-services.js │ │ ├── layout.js │ │ └── partials │ │ │ ├── files.html │ │ │ ├── footer.html │ │ │ ├── header.html │ │ │ ├── help.html │ │ │ └── navigation.html │ ├── libraries │ │ ├── LoDash.js │ │ └── libraries.js │ ├── models │ │ ├── DataModel.js │ │ └── models.js │ └── services │ │ ├── DataService.js │ │ ├── HttpStatusService.js │ │ ├── ListConfigService.js │ │ ├── MessageService.js │ │ ├── SocketHelperService.js │ │ └── services.js ├── examples │ ├── about │ │ ├── about.html │ │ └── about.js │ ├── author │ │ ├── add.html │ │ ├── author-controllers.js │ │ ├── author-models.js │ │ ├── author.html │ │ ├── author.js │ │ ├── list-info.html │ │ └── list.html │ ├── book │ │ ├── add.html │ │ ├── book-controllers.js │ │ ├── book-models.js │ │ ├── book.html │ │ ├── book.js │ │ ├── list-info.html │ │ └── list.html │ ├── chat │ │ ├── chat-controllers.js │ │ ├── chat-controllers_test.js │ │ ├── chat-directives.js │ │ ├── chat-info.html │ │ ├── chat-models.js │ │ ├── chat.html │ │ ├── chat.js │ │ └── chat.scss │ ├── examples.js │ └── messages │ │ ├── messages-controller.js │ │ ├── messages-info.html │ │ ├── messages.html │ │ └── messages.js ├── index.html └── styles │ ├── _angular.scss │ ├── _base.scss │ ├── _bootstrap.scss │ ├── _components.scss │ ├── _forms.scss │ ├── _mixins.scss │ └── modal.scss └── dummy.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "box-model": false, 3 | "fallback-colors": false, 4 | "box-sizing": false, 5 | "compatible-vendor-prefixes": false, 6 | "gradients": false, 7 | "adjoining-classes": false, 8 | "outline-none" : false 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | dist/* 4 | .tmp/ 5 | .idea/ 6 | config/config.json 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "bitwise": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "immed": true, 8 | "indent": 2, 9 | "newcap": true, 10 | "noarg": true, 11 | "quotmark": "single", 12 | "regexp": true, 13 | "undef": true, 14 | "unused": true, 15 | "strict": true, 16 | "trailing": true, 17 | "smarttabs": true, 18 | "globals": { 19 | "angular": false, 20 | "console": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.11" 5 | - "0.12" 6 | - "4.0" 7 | - "4.1" 8 | - "4.2" 9 | - "5.0" 10 | 11 | services: 12 | 13 | before_script: 14 | 15 | notifications: 16 | email: true 17 | 18 | # whitelisted branches 19 | branches: 20 | only: 21 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2015 Tarmo Leppänen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frontend side for angular-sailsjs-boilerplate 2 | [![GitHub version](https://badge.fury.io/gh/tarlepp%2Fangular-sailsjs-boilerplate-frontend.svg)](https://badge.fury.io/gh/tarlepp%2Fangular-sailsjs-boilerplate-frontend) 3 | [![Build Status](https://travis-ci.org/tarlepp/angular-sailsjs-boilerplate-frontend.png?branch=master)](https://travis-ci.org/tarlepp/angular-sailsjs-boilerplate-frontend) 4 | [![Dependency Status](https://david-dm.org/tarlepp/angular-sailsjs-boilerplate-frontend.svg)](https://david-dm.org/tarlepp/angular-sailsjs-boilerplate-frontend) 5 | [![devDependency Status](https://david-dm.org/tarlepp/angular-sailsjs-boilerplate-frontend/dev-status.svg)](https://david-dm.org/tarlepp/angular-sailsjs-boilerplate-frontend#info=devDependencies) 6 | 7 | This frontend code is used on [angular-sailsjs-boilerplate](https://github.com/tarlepp/angular-sailsjs-boilerplate) 8 | 9 | This is an example AngularJS application to demonstrate how to use separate back- and frontend applications. Currently 10 | this demo contains following features: 11 | 12 | * Login with backend 13 | * JWT token authentication after login 14 | * Simple list view (Books / Authors) to demonstrate socket communications 15 | * Generic error handler which is attached to $http and $sailsSocket 16 | * Message service to show specified messages to users 17 | * Live chat to demonstrate subscribe actions 18 | 19 | ## Used components 20 | This frontend application uses following 3rd party libraries to make all this magic happen. 21 | 22 | * slush-angular (https://github.com/slushjs/slush-angular) 23 | * AngularJS (https://github.com/angular/angular.js) 24 | * AngularUI Router (https://github.com/angular-ui/ui-router) 25 | * AngularUI Bootstrap (https://github.com/angular-ui/bootstrap) 26 | * angular-moment (https://github.com/urish/angular-moment) 27 | * Angular Bootstrap Show Errors (https://github.com/paulyoder/angular-bootstrap-show-errors) 28 | * angular-linkify (https://github.com/scottcorgan/angular-linkify) 29 | * angularSails (https://github.com/balderdashy/angularSails) 30 | * Bootstrap (https://github.com/twbs/bootstrap) 31 | * bootswatch (https://github.com/thomaspark/bootswatch/) 32 | * Font Awesome (https://github.com/FortAwesome/Font-Awesome) 33 | * noty - A jQuery Notification Plugin (https://github.com/needim/noty) 34 | * Sails JavaScript Client SDK (https://github.com/balderdashy/sails.io.js) 35 | 36 | ## Development 37 | 38 | To start developing in the project run: 39 | 40 | ```bash 41 | gulp serve 42 | ``` 43 | 44 | or 45 | 46 | ```bash 47 | npm start 48 | ``` 49 | 50 | Then head to `http://localhost:3001` in your browser. 51 | 52 | The `serve` tasks starts a static file server, which serves the AngularJS application, and a watch task which watches 53 | all files for changes and lints, builds and injects them into the index.html accordingly. 54 | 55 | ## Tests 56 | 57 | To run tests run: 58 | 59 | ```bash 60 | gulp test 61 | ``` 62 | 63 | **Or** first inject all test files into `karma.conf.js` with: 64 | 65 | ```bash 66 | gulp karma-conf 67 | ``` 68 | 69 | Then you're able to run Karma directly. Example: 70 | 71 | ```bash 72 | karma start --single-run 73 | ``` 74 | 75 | ## Production ready build - a.k.a. dist 76 | 77 | To make the app ready for deploy to production run: 78 | 79 | ```bash 80 | gulp dist 81 | ``` 82 | 83 | Now there's a `./dist` folder with all scripts and stylesheets concatenated and minified, also third party libraries 84 | installed with bower will be concatenated and minified into `vendors.min.js` and `vendors.min.css` respectively. 85 | 86 | To run your deployment code run: 87 | 88 | ```bash 89 | gulp production 90 | ``` 91 | 92 | Then head to `http://localhost:3000` in your browser. 93 | 94 | ## License 95 | The MIT License (MIT) 96 | 97 | Copyright (c) 2015 Tarmo Leppänen -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "main": [ 4 | "/dist/frontend.min.js", 5 | "/dist/frontend.min.css" 6 | ], 7 | "version": "0.1.0", 8 | "description": "Frontend application for angular-sailsjs-boilerplate", 9 | "authors": [ 10 | "Tarmo Leppänen" 11 | ], 12 | "ignore": [ 13 | "**/.*", 14 | "package.json", 15 | "klei.json", 16 | "gulpfile.js", 17 | "node_modules", 18 | "bower_components", 19 | "src" 20 | ], 21 | "dependencies": { 22 | "jquery": "2.1.3", 23 | "fontawesome": "4.3.0", 24 | "lodash": "3.2.0", 25 | "angular": "1.3.13", 26 | "angular-animate": "~1.3.13", 27 | "angular-loading-bar": "0.7.0", 28 | "angular-ui-router": "0.2.13", 29 | "angular-ui-bootstrap-bower": "0.12.1", 30 | "angular-ui-utils": "0.2.2", 31 | "angular-moment": "0.9.0", 32 | "angular-bootstrap-show-errors": "2.3.0", 33 | "angular-sanitize": "1.3.13", 34 | "angular-xeditable": "0.1.8", 35 | "angular-toastr": "1.0.1", 36 | "textAngular": "1.3.7", 37 | "bootbox": "4.4.0", 38 | "ngBootbox": "0.0.4", 39 | "sails.io.js": "0.11.5", 40 | "angularSails": "master", 41 | "ngstorage": "~0.3.0", 42 | "highcharts": "~4.1.3", 43 | "highcharts-ng": "~0.0.8" 44 | }, 45 | "devDependencies": { 46 | "angular-mocks": "1.3.13" 47 | }, 48 | "overrides": { 49 | "fontawesome": { 50 | "main": [ 51 | "css/font-awesome.min.css", 52 | "fonts/FontAwesome.otf", 53 | "fonts/fontawesome-webfont.eot", 54 | "fonts/fontawesome-webfont.swg", 55 | "fonts/fontawesome-webfont.ttf", 56 | "fonts/fontawesome-webfont.svg" 57 | ] 58 | }, 59 | "ngBootbox": { 60 | "main": "dist/ngBootbox.js" 61 | } 62 | }, 63 | "resolutions": { 64 | "angular": "1.3.13", 65 | "bootbox": "4.4.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | Frontend config 2 | ============ 3 | 4 | This folder contains example of frontend config JSON file. By default folder contains just an example file for this. 5 | If you need to override default values just copy ```config_example.json``` to ```config.json``` and make necessary 6 | changes to it. Note that ```config.json``` is ignored from vcs. 7 | 8 | Currently this configration file contains following values for frontend side: 9 | 10 |
11 | backendUrl = Sails backend URL, defaults to http://localhost:1337
12 | 
-------------------------------------------------------------------------------- /config/config_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "backendUrl": "http://localhost:1337", 3 | "frontend": { 4 | "ports": { 5 | "production": 3000, 6 | "development": 3001 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var gulp = require('gulp'); 5 | var replace = require('gulp-replace-task'); 6 | var fs = require('fs'); 7 | var g = require('gulp-load-plugins')({lazy: false}); 8 | var noop = g.util.noop; 9 | var es = require('event-stream'); 10 | var Queue = require('streamqueue'); 11 | var lazypipe = require('lazypipe'); 12 | var stylish = require('jshint-stylish'); 13 | var bower = require('./bower'); 14 | var mainBowerFiles = require('main-bower-files'); 15 | var historyApiFallback = require('connect-history-api-fallback'); 16 | var isWatching = false; 17 | 18 | var htmlminOpts = { 19 | removeComments: true, 20 | collapseWhitespace: true, 21 | removeEmptyAttributes: false, 22 | collapseBooleanAttributes: true, 23 | removeRedundantAttributes: true 24 | }; 25 | 26 | var settings; 27 | 28 | // Try to read frontend configuration file, fallback to default file 29 | try { 30 | settings = JSON.parse(fs.readFileSync('./config/config.json', 'utf8')); 31 | } catch (error) { 32 | settings = JSON.parse(fs.readFileSync('./config/config_example.json', 'utf8')); 33 | } 34 | 35 | /** 36 | * JS Hint 37 | */ 38 | gulp.task('jshint', function() { 39 | return gulp.src([ 40 | './gulpfile.js', 41 | './src/app/**/*.js' 42 | ]) 43 | .pipe(g.cached('jshint')) 44 | .pipe(jshint('./.jshintrc')) 45 | .pipe(livereload()); 46 | }); 47 | 48 | /** 49 | * CSS 50 | */ 51 | gulp.task('clean-css', function() { 52 | return gulp.src('./.tmp/css').pipe(g.clean()); 53 | }); 54 | 55 | gulp.task('styles', ['clean-css'], function() { 56 | return gulp.src([ 57 | './src/app/**/*.scss', 58 | '!./src/app/**/_*.scss' 59 | ]) 60 | .pipe(g.sass()) 61 | .pipe(gulp.dest('./.tmp/css/')) 62 | .pipe(g.cached('built-css')) 63 | .pipe(livereload()); 64 | }); 65 | 66 | gulp.task('styles-dist', ['styles'], function() { 67 | return cssFiles().pipe(dist('css', bower.name)); 68 | }); 69 | 70 | gulp.task('csslint', ['styles'], function() { 71 | return cssFiles() 72 | .pipe(g.cached('csslint')) 73 | .pipe(g.csslint('./.csslintrc')) 74 | .pipe(g.csslint.reporter()) 75 | ; 76 | }); 77 | 78 | gulp.task('scsslint', ['styles'], function() { 79 | var scsslint = g.scssLint; 80 | 81 | return gulp.src('./src/app/**/*.scss') 82 | .pipe(scsslint({ 83 | config: './scsslint.yml' 84 | })); 85 | }); 86 | 87 | /** 88 | * Scripts 89 | */ 90 | gulp.task('scripts-dist', ['templates-dist'], function() { 91 | return appFiles().pipe(dist('js', bower.name, {ngAnnotate: true})); 92 | }); 93 | 94 | /** 95 | * Templates 96 | */ 97 | gulp.task('templates', function() { 98 | return templateFiles().pipe(buildTemplates()); 99 | }); 100 | 101 | gulp.task('templates-dist', function() { 102 | return templateFiles({min: true}).pipe(buildTemplates()); 103 | }); 104 | 105 | /** 106 | * Vendors 107 | */ 108 | gulp.task('vendors', function() { 109 | var bowerStream = gulp.src(mainBowerFiles()); 110 | 111 | return es.merge( 112 | bowerStream.pipe(g.filter('**/*.css')).pipe(dist('css', 'vendors')), 113 | bowerStream.pipe(g.filter('**/*.js')).pipe(dist('js', 'vendors')) 114 | ); 115 | }); 116 | 117 | /** 118 | * Index 119 | */ 120 | gulp.task('index', index); 121 | gulp.task('build-all', ['styles', 'templates'], index); 122 | 123 | function index() { 124 | var opt = {read: false}; 125 | 126 | return gulp.src('./src/app/index.html') 127 | .pipe(g.inject(gulp.src(mainBowerFiles(opt)), {ignorePath: 'bower_components', starttag: ''})) 128 | .pipe(g.inject(es.merge(appFiles(), cssFiles(opt)), {ignorePath: ['.tmp', 'src/app']})) 129 | .pipe(g.embedlr()) 130 | .pipe(replace({ 131 | patterns: [ 132 | { 133 | match: 'backendUrl', 134 | replacement: settings.backendUrl 135 | } 136 | ] 137 | })) 138 | .pipe(gulp.dest('./.tmp/')) 139 | .pipe(livereload()) 140 | ; 141 | } 142 | 143 | /** 144 | * Assets 145 | */ 146 | gulp.task('assets', function() { 147 | return gulp.src('./src/app/assets/**') 148 | .pipe(gulp.dest('./dist/assets')) 149 | ; 150 | }); 151 | 152 | /** 153 | * Partials 154 | */ 155 | gulp.task('partials', function() { 156 | return gulp.src('./src/app/partials/**') 157 | .pipe(gulp.dest('./dist/partials')) 158 | ; 159 | }); 160 | 161 | /** 162 | * Fonts 163 | */ 164 | gulp.task('fonts', function() { 165 | return gulp.src('./bower_components/fontawesome/fonts/**') 166 | .pipe(gulp.dest('./dist/fonts')) 167 | ; 168 | }); 169 | 170 | /** 171 | * Dist 172 | */ 173 | gulp.task('dist', ['vendors', 'assets', 'fonts', 'styles-dist', 'scripts-dist'], function() { 174 | return gulp.src('./src/app/index.html') 175 | .pipe(g.inject(gulp.src('./dist/vendors.min.{js,css}'), { 176 | ignorePath: 'dist', 177 | starttag: '' 178 | })) 179 | .pipe(g.inject(gulp.src('./dist/' + bower.name + '.min.{js,css}'), {ignorePath: 'dist'})) 180 | .pipe(replace({ 181 | patterns: [ 182 | { 183 | match: 'backendUrl', 184 | replacement: settings.backendUrl 185 | } 186 | ] 187 | })) 188 | .pipe(g.htmlmin(htmlminOpts)) 189 | .pipe(gulp.dest('./dist/')) 190 | ; 191 | }); 192 | 193 | /** 194 | * Static file server 195 | */ 196 | gulp.task('statics', g.serve({ 197 | port: settings.frontend.ports.development, 198 | root: ['./.tmp', './src/app', './bower_components'], 199 | middleware: historyApiFallback({}) 200 | })); 201 | 202 | /** 203 | * Production file server, note remember to run 'gulp dist' first! 204 | */ 205 | gulp.task('production', g.serve({ 206 | port: settings.frontend.ports.production, 207 | root: ['./dist'], 208 | middleware: historyApiFallback({}) 209 | })); 210 | 211 | /** 212 | * Watch 213 | */ 214 | gulp.task('serve', ['watch']); 215 | 216 | gulp.task('watch', ['statics', 'default'], function() { 217 | isWatching = true; 218 | 219 | // Initiate livereload server: 220 | g.livereload({ 221 | start: true 222 | }); 223 | 224 | gulp.watch('./src/app/**/*.js', ['jshint']).on('change', function(evt) { 225 | if (evt.type !== 'changed') { 226 | gulp.start('index'); 227 | } 228 | }); 229 | 230 | gulp.watch('./src/app/index.html', ['index']); 231 | gulp.watch(['./src/app/**/*.html', '!./src/app/index.html'], ['templates']); 232 | gulp.watch(['./src/app/**/*.scss'], ['csslint', 'scsslint']).on('change', function(evt) { 233 | if (evt.type !== 'changed') { 234 | gulp.start('index'); 235 | } 236 | }); 237 | }); 238 | 239 | /** 240 | * Default task 241 | */ 242 | gulp.task('default', ['lint', 'build-all']); 243 | 244 | /** 245 | * Lint everything 246 | */ 247 | gulp.task('lint', ['jshint', 'csslint', 'scsslint']); 248 | 249 | /** 250 | * Test 251 | */ 252 | gulp.task('test', ['templates'], function() { 253 | return testFiles() 254 | .pipe(g.karma({ 255 | configFile: __dirname + '/karma.conf.js', 256 | singleRun: true 257 | })) 258 | ; 259 | }); 260 | 261 | /** 262 | * Inject all files for tests into karma.conf.js 263 | * to be able to run `karma` without gulp. 264 | */ 265 | gulp.task('karma-conf', ['templates'], function() { 266 | return gulp.src('./karma.conf.js') 267 | .pipe(g.inject(testFiles(), { 268 | starttag: 'files: [', 269 | endtag: ']', 270 | addRootSlash: false, 271 | transform: function(filepath, file, i, length) { 272 | return ' \'' + filepath + '\'' + (i + 1 < length ? ',' : ''); 273 | } 274 | })) 275 | .pipe(gulp.dest('./')) 276 | ; 277 | }); 278 | 279 | /** 280 | * Test files 281 | */ 282 | function testFiles() { 283 | return new Queue({objectMode: true}) 284 | .Queue(gulp.src(mainBowerFiles()).pipe(g.filter('**/*.js'))) 285 | .Queue(gulp.src('./bower_components/angular-mocks/angular-mocks.js')) 286 | .Queue(appFiles()) 287 | .Queue(gulp.src('./src/app/**/*_test.js')) 288 | .done() 289 | ; 290 | } 291 | 292 | /** 293 | * All CSS files as a stream 294 | */ 295 | function cssFiles(opt) { 296 | return gulp.src('./.tmp/css/**/*.css', opt); 297 | } 298 | 299 | /** 300 | * All AngularJS application files as a stream 301 | */ 302 | function appFiles() { 303 | var files = [ 304 | './.tmp/' + bower.name + '-templates.js', 305 | './src/app/**/*.js', 306 | '!./src/app/**/*_test.js' 307 | ]; 308 | 309 | return gulp.src(files) 310 | .pipe(g.angularFilesort()) 311 | ; 312 | } 313 | 314 | /** 315 | * All AngularJS templates/partials as a stream 316 | */ 317 | function templateFiles(opt) { 318 | return gulp.src(['./src/app/**/*.html', '!./src/app/index.html'], opt) 319 | .pipe(opt && opt.min ? g.htmlmin(htmlminOpts) : noop()) 320 | ; 321 | } 322 | 323 | /** 324 | * Build AngularJS templates/partials 325 | */ 326 | function buildTemplates() { 327 | return lazypipe() 328 | .pipe(g.ngHtml2js, { 329 | moduleName: bower.name + '-templates', 330 | prefix: '/' + bower.name + '/', 331 | stripPrefix: '/src/app' 332 | }) 333 | .pipe(g.concat, bower.name + '-templates.js') 334 | .pipe(gulp.dest, './.tmp') 335 | .pipe(livereload)() 336 | ; 337 | } 338 | 339 | /** 340 | * Concat, rename, minify 341 | * 342 | * @param {String} ext 343 | * @param {String} name 344 | * @param {Object} opt 345 | */ 346 | function dist(ext, name, opt) { 347 | opt = opt || {}; 348 | 349 | return lazypipe() 350 | .pipe(g.concat, name + '.' + ext) 351 | .pipe(gulp.dest, './dist') 352 | .pipe(opt.ngAnnotate ? g.ngAnnotate : noop) 353 | .pipe(opt.ngAnnotate ? g.rename : noop, name + '.annotated.' + ext) 354 | .pipe(opt.ngAnnotate ? gulp.dest : noop, './dist') 355 | .pipe(ext === 'js' ? g.uglify : g.minifyCss) 356 | .pipe(g.rename, name + '.min.' + ext) 357 | .pipe(gulp.dest, './dist')() 358 | ; 359 | } 360 | 361 | /** 362 | * Livereload (or noop if not run by watch) 363 | */ 364 | function livereload() { 365 | return lazypipe() 366 | .pipe(isWatching ? g.livereload : noop)() 367 | ; 368 | } 369 | 370 | /** 371 | * Jshint with stylish reporter 372 | */ 373 | function jshint(jshintfile) { 374 | // Read JSHint settings, for some reason jshint-stylish won't work on initial load of files 375 | var jshintSettings = JSON.parse(fs.readFileSync(jshintfile, 'utf8')); 376 | 377 | return lazypipe() 378 | .pipe(g.jshint, jshintSettings) 379 | .pipe(g.jshint.reporter, stylish)() 380 | ; 381 | } 382 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function ( karma ) { 3 | process.env.PHANTOMJS_BIN = 'node_modules/phantomjs/bin/phantomjs'; 4 | 5 | karma.set({ 6 | /** 7 | * From where to look for files, starting with the location of this file. 8 | */ 9 | basePath: './', 10 | 11 | /** 12 | * Filled by the task `gulp karma-conf` 13 | */ 14 | files: [ 15 | ], 16 | 17 | frameworks: [ 'mocha', 'chai', 'sinon-chai' ], 18 | plugins: [ 'karma-mocha', 'karma-mocha-reporter', 'karma-chai', 'karma-sinon-chai', 'karma-phantomjs-launcher' ], 19 | 20 | /** 21 | * How to report, by default. 22 | */ 23 | reporters: 'mocha', 24 | 25 | /** 26 | * Show colors in output? 27 | */ 28 | colors: true, 29 | 30 | /** 31 | * On which port should the browser connect, on which port is the test runner 32 | * operating, and what is the URL path for the browser to use. 33 | */ 34 | port: 9099, 35 | runnerPort: 9100, 36 | urlRoot: '/', 37 | 38 | /** 39 | * Disable file watching by default. 40 | */ 41 | autoWatch: false, 42 | 43 | /** 44 | * The list of browsers to launch to test on. This includes only "Firefox" by 45 | * default, but other browser names include: 46 | * Chrome, ChromeCanary, Firefox, Opera, Safari, PhantomJS 47 | * 48 | * Note that you can also use the executable name of the browser, like "chromium" 49 | * or "firefox", but that these vary based on your operating system. 50 | * 51 | * You may also leave this blank and manually navigate your browser to 52 | * http://localhost:9099/ when you're running tests. The window/tab can be left 53 | * open and the tests will automatically occur there during the build. This has 54 | * the aesthetic advantage of not launching a browser every time you save. 55 | */ 56 | browsers: [ 57 | 'PhantomJS' 58 | ] 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.2.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "gulp watch", 7 | "test": "gulp test", 8 | "postinstall": "./node_modules/bower/bin/bower install" 9 | }, 10 | "keywords": [ 11 | "slush, angular, slush-angular" 12 | ], 13 | "dependencies": { 14 | "bower": "1.7.7", 15 | "connect-history-api-fallback": "1.2.0" 16 | }, 17 | "devDependencies": { 18 | "chai": "3.4.1", 19 | "event-stream": "3.3.2", 20 | "gulp": "3.9.1", 21 | "gulp-angular-filesort": "1.1.1", 22 | "gulp-cached": "1.1.0", 23 | "gulp-clean": "0.3.2", 24 | "gulp-concat": "2.6.0", 25 | "gulp-csslint": "0.2.2", 26 | "gulp-embedlr": "0.5.2", 27 | "gulp-filter": "4.0.0", 28 | "gulp-header": "1.7.1", 29 | "gulp-htmlmin": "1.3.0", 30 | "gulp-inject": "4.0.0", 31 | "gulp-jshint": "2.0.0", 32 | "gulp-karma": "0.0.5", 33 | "gulp-livereload": "3.8.1", 34 | "gulp-load-plugins": "1.2.0", 35 | "gulp-minify-css": "1.2.4", 36 | "gulp-ng-annotate": "2.0.0", 37 | "gulp-ng-html2js": "0.2.2", 38 | "gulp-rename": "1.2.2", 39 | "gulp-replace-task": "0.11.0", 40 | "gulp-sass": "2.2.0", 41 | "gulp-scss-lint": "0.3.9", 42 | "gulp-serve": "1.2.0", 43 | "gulp-uglify": "1.5.3", 44 | "gulp-util": "3.0.7", 45 | "jshint": "2.9.1", 46 | "jshint-stylish": "2.1.0", 47 | "karma": "0.13.15", 48 | "karma-chai": "0.1.0", 49 | "karma-chrome-launcher": "0.2.1", 50 | "karma-mocha": "0.2.0", 51 | "karma-mocha-reporter": "1.1.1", 52 | "karma-phantomjs-launcher": "0.2.1", 53 | "karma-sinon-chai": "1.1.0", 54 | "lazypipe": "1.0.1", 55 | "main-bower-files": "2.9.0", 56 | "mocha": "2.3.3", 57 | "phantomjs": "1.9.18", 58 | "sinon": "1.17.2", 59 | "sort-stream": "1.0.1", 60 | "streamqueue": "1.1.1", 61 | "tiny-lr": "0.2.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /scsslint.yml: -------------------------------------------------------------------------------- 1 | # Default application configuration that all configurations inherit from. 2 | 3 | scss_files: "**/*.scss" 4 | plugin_directories: ['.scss-linters'] 5 | 6 | # List of gem names to load custom linters from (make sure they are already 7 | # installed) 8 | plugin_gems: [] 9 | 10 | linters: 11 | BangFormat: 12 | enabled: true 13 | space_before_bang: true 14 | space_after_bang: false 15 | 16 | BemDepth: 17 | enabled: false 18 | max_elements: 1 19 | 20 | BorderZero: 21 | enabled: true 22 | convention: zero # or `none` 23 | 24 | ColorKeyword: 25 | enabled: true 26 | 27 | ColorVariable: 28 | enabled: true 29 | 30 | Comment: 31 | enabled: true 32 | 33 | DebugStatement: 34 | enabled: true 35 | 36 | DeclarationOrder: 37 | enabled: true 38 | 39 | DisableLinterReason: 40 | enabled: false 41 | 42 | DuplicateProperty: 43 | enabled: true 44 | 45 | ElsePlacement: 46 | enabled: true 47 | style: same_line # or 'new_line' 48 | 49 | EmptyLineBetweenBlocks: 50 | enabled: true 51 | ignore_single_line_blocks: true 52 | 53 | EmptyRule: 54 | enabled: true 55 | 56 | ExtendDirective: 57 | enabled: false 58 | 59 | FinalNewline: 60 | enabled: true 61 | present: true 62 | 63 | HexLength: 64 | enabled: true 65 | style: short # or 'long' 66 | 67 | HexNotation: 68 | enabled: true 69 | style: lowercase # or 'uppercase' 70 | 71 | HexValidation: 72 | enabled: true 73 | 74 | IdSelector: 75 | enabled: true 76 | 77 | ImportantRule: 78 | enabled: true 79 | 80 | ImportPath: 81 | enabled: true 82 | leading_underscore: false 83 | filename_extension: false 84 | 85 | Indentation: 86 | enabled: true 87 | allow_non_nested_indentation: false 88 | character: space # or 'tab' 89 | width: 2 90 | 91 | LeadingZero: 92 | enabled: true 93 | style: exclude_zero # or 'include_zero' 94 | 95 | MergeableSelector: 96 | enabled: true 97 | force_nesting: true 98 | 99 | NameFormat: 100 | enabled: true 101 | allow_leading_underscore: true 102 | convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern 103 | 104 | NestingDepth: 105 | enabled: true 106 | max_depth: 10 107 | ignore_parent_selectors: false 108 | 109 | PlaceholderInExtend: 110 | enabled: true 111 | 112 | PropertyCount: 113 | enabled: false 114 | include_nested: false 115 | max_properties: 10 116 | 117 | PropertySortOrder: 118 | enabled: true 119 | ignore_unspecified: false 120 | min_properties: 2 121 | separate_groups: false 122 | 123 | PropertySpelling: 124 | enabled: true 125 | extra_properties: [] 126 | 127 | PropertyUnits: 128 | enabled: true 129 | global: [ 130 | 'ch', 'em', 'ex', 'rem', # Font-relative lengths 131 | 'cm', 'in', 'mm', 'pc', 'pt', 'px', 'q', # Absolute lengths 132 | 'vh', 'vw', 'vmin', 'vmax', # Viewport-percentage lengths 133 | 'deg', 'grad', 'rad', 'turn', # Angle 134 | 'ms', 's', # Duration 135 | 'Hz', 'kHz', # Frequency 136 | 'dpi', 'dpcm', 'dppx', # Resolution 137 | '%'] # Other 138 | properties: {} 139 | 140 | QualifyingElement: 141 | enabled: true 142 | allow_element_with_attribute: false 143 | allow_element_with_class: false 144 | allow_element_with_id: false 145 | 146 | SelectorDepth: 147 | enabled: true 148 | max_depth: 6 149 | 150 | SelectorFormat: 151 | enabled: true 152 | convention: hyphenated_lowercase # or 'strict_BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern 153 | 154 | Shorthand: 155 | enabled: true 156 | allowed_shorthands: [1, 2, 3] 157 | 158 | SingleLinePerProperty: 159 | enabled: true 160 | allow_single_line_rule_sets: true 161 | 162 | SingleLinePerSelector: 163 | enabled: true 164 | 165 | SpaceAfterComma: 166 | enabled: true 167 | style: one_space # or 'no_space', or 'at_least_one_space' 168 | 169 | SpaceAfterPropertyColon: 170 | enabled: true 171 | style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' 172 | 173 | SpaceAfterPropertyName: 174 | enabled: true 175 | 176 | SpaceAfterVariableName: 177 | enabled: true 178 | 179 | SpaceAroundOperator: 180 | enabled: true 181 | style: one_space # or 'no_space' 182 | 183 | SpaceBeforeBrace: 184 | enabled: true 185 | style: space # or 'new_line' 186 | allow_single_line_padding: false 187 | 188 | SpaceBetweenParens: 189 | enabled: true 190 | spaces: 0 191 | 192 | StringQuotes: 193 | enabled: true 194 | style: single_quotes # or double_quotes 195 | 196 | TrailingSemicolon: 197 | enabled: true 198 | 199 | TrailingWhitespace: 200 | enabled: true 201 | 202 | TrailingZero: 203 | enabled: false 204 | 205 | TransitionAll: 206 | enabled: false 207 | 208 | UnnecessaryMantissa: 209 | enabled: true 210 | 211 | UnnecessaryParentReference: 212 | enabled: true 213 | 214 | UrlFormat: 215 | enabled: true 216 | 217 | UrlQuotes: 218 | enabled: true 219 | 220 | VariableForProperty: 221 | enabled: false 222 | properties: [] 223 | 224 | VendorPrefix: 225 | enabled: true 226 | identifier_list: base 227 | additional_identifiers: [] 228 | excluded_identifiers: [] 229 | 230 | ZeroUnit: 231 | enabled: true 232 | 233 | Compass::*: 234 | enabled: false -------------------------------------------------------------------------------- /src/app/admin/admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular module for admin component. All of these are wrapped to 'frontend.admin.login-history' angular module. This 3 | * component is divided to following logical components: 4 | * 5 | * frontend.admin.login-history 6 | * 7 | * Also this file contains all necessary information about 'frontend.admin' module route definitions. 8 | */ 9 | (function() { 10 | 'use strict'; 11 | 12 | // Define frontend.admin module 13 | angular.module('frontend.admin', [ 14 | 'frontend.admin.login-history' 15 | ]); 16 | 17 | // Module configuration 18 | angular.module('frontend.admin') 19 | .config([ 20 | '$stateProvider', 21 | function config($stateProvider) { 22 | $stateProvider 23 | .state('admin', { 24 | parent: 'frontend', 25 | data: { 26 | access: 2 27 | }, 28 | views: { 29 | 'content@': { 30 | controller: [ 31 | '$state', 32 | function($state) { 33 | $state.go('admin.login-history'); 34 | } 35 | ] 36 | }, 37 | 'pageNavigation@': { 38 | templateUrl: '/frontend/core/layout/partials/navigation.html', 39 | controller: 'NavigationController', 40 | resolve: { 41 | _items: [ 42 | 'ContentNavigationItems', 43 | function resolve(ContentNavigationItems) { 44 | return ContentNavigationItems.getItems('admin'); 45 | } 46 | ] 47 | } 48 | } 49 | } 50 | }) 51 | ; 52 | } 53 | ]) 54 | ; 55 | }()); 56 | -------------------------------------------------------------------------------- /src/app/admin/loginHistory/index.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | User login history ({{itemCount}}) 4 | 5 | 21 |

22 | 23 | 24 | 25 | 26 | 44 | 45 | 46 | 47 | 48 | 51 | 54 | 57 | 64 | 68 | 69 | 70 | 71 | 74 | 75 | 76 |
29 | 34 | 38 | 39 | 43 |
49 | {{item.ip}} 50 | 52 | {{item.browser}} 53 | 55 | {{item.os}} 56 | 58 | {{item.user.lastName}}, {{item.user.firstName}} 59 | 60 | 61 | ({{item.user.username}}) 62 | 63 | 65 | {{item.createdAt | amDateFormat : 'LLLL'}}, 66 | 67 |
72 | no data founded... 73 |
77 | 78 | 86 | 87 |
88 | 89 |
90 |
91 | 92 |
93 |
94 | 95 |
96 |
97 | 98 |
99 |
-------------------------------------------------------------------------------- /src/app/admin/loginHistory/login-history-controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular controller definitions for 'frontend.admin.login-history' module. 3 | * 4 | * Note that this file should only contain controllers and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | angular.module('frontend.admin.login-history') 10 | .controller('LoginHistoryController', [ 11 | '$scope', '$timeout', '$q', '$filter', 12 | '_', 13 | 'ListConfig', 14 | 'SocketHelperService', 15 | 'LoginHistoryModel', 16 | '_items', '_count', '_statsBrowser', '_statsOS', '_statsUser', 17 | function controller( 18 | $scope, $timeout, $q, $filter, 19 | _, 20 | ListConfig, 21 | SocketHelperService, 22 | LoginHistoryModel, 23 | _items, _count, _statsBrowser, _statsOS, _statsUser 24 | ) { 25 | // Set current scope reference to models 26 | LoginHistoryModel.setScope($scope, false, 'items', 'itemCount'); 27 | 28 | // Store statistics 29 | $scope.statsBrowser = _statsBrowser; 30 | $scope.statsOS = _statsOS; 31 | $scope.statsUser = _statsUser; 32 | 33 | // Add default list configuration variable to current scope 34 | $scope = angular.extend($scope, angular.copy(ListConfig.getConfig())); 35 | 36 | // Set initial data 37 | $scope.items = _items; 38 | $scope.itemCount = _count.count; 39 | 40 | // Initialize used title items 41 | $scope.titleItems = ListConfig.getTitleItems(LoginHistoryModel.endpoint); 42 | 43 | // Initialize default sort data 44 | $scope.sort = { 45 | column: 'createdAt', 46 | direction: false 47 | }; 48 | 49 | // Initialize filters 50 | $scope.filters = { 51 | searchWord: '', 52 | columns: $scope.titleItems 53 | }; 54 | 55 | // Define default chart configuration for each statistics chart 56 | var chartConfig = { 57 | options: { 58 | chart: { 59 | type: 'pie' 60 | }, 61 | plotOptions: { 62 | pie: { 63 | allowPointSelect: true, 64 | cursor: 'pointer', 65 | dataLabels: { 66 | enabled: false 67 | }, 68 | showInLegend: true 69 | } 70 | }, 71 | exporting: { 72 | enabled: false 73 | }, 74 | tooltip: { 75 | formatter: function formatter() { 76 | return '' + this.key + '
' + 77 | 'Percentage: ' + $filter('number')(this.point.percentage, 2) + '%
' + 78 | 'Total: ' + $filter('number')(this.y) 79 | ; 80 | }, 81 | pointFormat: '{series.name}: {point.percentage:.1f}%' 82 | } 83 | }, 84 | title: { 85 | text: '' 86 | }, 87 | series: [{ 88 | type: 'pie', 89 | name: '', 90 | data: [] 91 | }] 92 | }; 93 | 94 | var charts = [ 95 | { 96 | scope: 'chartBrowser', 97 | data: 'statsBrowser', 98 | title: 'Browsers' 99 | }, 100 | { 101 | scope: 'chartOs', 102 | data: 'statsOS', 103 | title: 'Operating systems' 104 | }, 105 | { 106 | scope: 'chartUser', 107 | data: 'statsUser', 108 | title: 'Users' 109 | } 110 | ]; 111 | 112 | _.forEach(charts, function iterator(config) { 113 | $scope[config.scope] = angular.copy(chartConfig); 114 | 115 | $scope[config.scope].series[0].data = $scope[config.data]; 116 | $scope[config.scope].title.text = config.title; 117 | }); 118 | 119 | // Function to change sort column / direction on list 120 | $scope.changeSort = function changeSort(item) { 121 | var sort = $scope.sort; 122 | 123 | if (sort.column === item.column) { 124 | sort.direction = !sort.direction; 125 | } else { 126 | sort.column = item.column; 127 | sort.direction = true; 128 | } 129 | 130 | _triggerFetchData(); 131 | }; 132 | 133 | // Watcher for items, this is needed to update charts 134 | $scope.$watch('items', function watcher(valueNew, valueOld) { 135 | if (valueNew !== valueOld) { 136 | var actions = [ 137 | { 138 | action: 'Browser', 139 | scope: 'chartBrowser' 140 | }, 141 | { 142 | action: 'OS', 143 | scope: 'chartOs' 144 | }, 145 | { 146 | action: 'User', 147 | scope: 'chartUser' 148 | } 149 | ]; 150 | 151 | // Create necessary promises to update chart data 152 | var promises = _.map(actions, function iterator(config) { 153 | LoginHistoryModel 154 | .statistics(config.action) 155 | .then( 156 | function onSuccess(data) { 157 | $scope[config.scope].series[0].data = data; 158 | } 159 | ) 160 | ; 161 | }); 162 | 163 | // Execute all promises 164 | $q.all(promises); 165 | } 166 | }); 167 | 168 | // Simple watcher for 'currentPage' scope variable. If this is changed we need to fetch book data from server. 169 | $scope.$watch('currentPage', function watcher(valueNew, valueOld) { 170 | if (valueNew !== valueOld) { 171 | _fetchData(); 172 | } 173 | }); 174 | 175 | // Simple watcher for 'itemsPerPage' scope variable. If this is changed we need to fetch book data from server. 176 | $scope.$watch('itemsPerPage', function watcher(valueNew, valueOld) { 177 | if (valueNew !== valueOld) { 178 | _triggerFetchData(); 179 | } 180 | }); 181 | 182 | var searchWordTimer; 183 | 184 | /** 185 | * Watcher for 'filter' scope variable, which contains multiple values that we're interested 186 | * within actual GUI. This will trigger new data fetch query to server if following conditions 187 | * have been met: 188 | * 189 | * 1) Actual filter variable is different than old one 190 | * 2) Search word have not been changed in 400ms 191 | * 192 | * If those are ok, then watcher will call 'fetchData' function. 193 | */ 194 | $scope.$watch('filters', function watcher(valueNew, valueOld) { 195 | if (valueNew !== valueOld) { 196 | if (searchWordTimer) { 197 | $timeout.cancel(searchWordTimer); 198 | } 199 | 200 | searchWordTimer = $timeout(_triggerFetchData, 400); 201 | } 202 | }, true); 203 | 204 | /** 205 | * Helper function to trigger actual data fetch from backend. This will just check current page 206 | * scope variable and if it is 1 call 'fetchData' function right away. Any other case just set 207 | * 'currentPage' scope variable to 1, which will trigger watcher to fetch data. 208 | * 209 | * @private 210 | */ 211 | function _triggerFetchData() { 212 | if ($scope.currentPage === 1) { 213 | _fetchData(); 214 | } else { 215 | $scope.currentPage = 1; 216 | } 217 | } 218 | 219 | /** 220 | * Helper function to fetch actual data for GUI from backend server with current parameters: 221 | * 1) Current page 222 | * 2) Search word 223 | * 3) Sort order 224 | * 4) Items per page 225 | * 226 | * Actually this function is doing two request to backend: 227 | * 1) Data count by given filter parameters 228 | * 2) Actual data fetch for current page with filter parameters 229 | * 230 | * These are fetched via 'LoginHistoryModel' service with promises. 231 | * 232 | * @private 233 | */ 234 | function _fetchData() { 235 | $scope.loading = true; 236 | 237 | // Common parameters for count and data query 238 | var commonParameters = { 239 | where: SocketHelperService.getWhere($scope.filters), 240 | populate: 'user' 241 | }; 242 | 243 | // Data query specified parameters 244 | var parameters = { 245 | limit: $scope.itemsPerPage, 246 | skip: ($scope.currentPage - 1) * $scope.itemsPerPage, 247 | sort: $scope.sort.column + ' ' + ($scope.sort.direction ? 'ASC' : 'DESC') 248 | }; 249 | 250 | // Fetch data count 251 | var count = LoginHistoryModel 252 | .count(commonParameters) 253 | .then( 254 | function onSuccess(response) { 255 | $scope.itemCount = response.count; 256 | } 257 | ) 258 | ; 259 | 260 | // Fetch actual data 261 | var load = LoginHistoryModel 262 | .load(_.merge({}, commonParameters, parameters)) 263 | .then( 264 | function onSuccess(response) { 265 | $scope.items = response; 266 | } 267 | ) 268 | ; 269 | 270 | // Load all needed data 271 | $q 272 | .all([count, load]) 273 | .finally( 274 | function onFinally() { 275 | $scope.loaded = true; 276 | $scope.loading = false; 277 | } 278 | ) 279 | ; 280 | } 281 | } 282 | ]) 283 | ; 284 | }()); 285 | -------------------------------------------------------------------------------- /src/app/admin/loginHistory/login-history-models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular model definitions for 'frontend.admin.login-history' module. 3 | * 4 | * Note that this file should only contain models and nothing else. Also note that these "models" are just basically 5 | * services that wraps all things together. 6 | */ 7 | (function () { 8 | 'use strict'; 9 | 10 | /** 11 | * Model for Book API, this is used to wrap all Book objects specified actions and data change actions. 12 | */ 13 | angular.module('frontend.admin.login-history') 14 | .factory('LoginHistoryModel', [ 15 | '$log', 16 | 'DataModel', 'DataService', 17 | function factory( 18 | $log, 19 | DataModel, DataService 20 | ) { 21 | var model = new DataModel('userlogin'); 22 | 23 | model.statistics = function statistics(type) { 24 | var self = this; 25 | 26 | return DataService 27 | .collection(self.endpoint + '/statistics/', {type: type}) 28 | .then( 29 | function onSuccess(response) { 30 | return response.data; 31 | }, 32 | function onError(error) { 33 | $log.error('LoginHistoryModel.statistics() failed.', error, self.endpoint, type); 34 | } 35 | ) 36 | ; 37 | }; 38 | 39 | return model; 40 | } 41 | ]) 42 | ; 43 | }()); 44 | -------------------------------------------------------------------------------- /src/app/admin/loginHistory/login-history.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Login history component. This component is divided to following logical components: 3 | * 4 | * Controllers 5 | * 6 | * All of these are wrapped to 'frontend.admin.login-history' angular module. This also contains necessary route 7 | * definitions for this module. 8 | */ 9 | (function() { 10 | 'use strict'; 11 | 12 | // Define frontend.admin module.login-history 13 | angular.module('frontend.admin.login-history', []); 14 | 15 | // Module configuration 16 | angular.module('frontend.admin.login-history') 17 | .config([ 18 | '$stateProvider', 19 | function config($stateProvider) { 20 | $stateProvider 21 | .state('admin.login-history', { 22 | url: '/admin/loginHistory', 23 | views: { 24 | 'content@': { 25 | templateUrl: '/frontend/admin/loginHistory/index.html', 26 | controller: 'LoginHistoryController', 27 | resolve: { 28 | _items: [ 29 | 'ListConfig', 30 | 'LoginHistoryModel', 31 | function resolve( 32 | ListConfig, 33 | LoginHistoryModel 34 | ) { 35 | var config = ListConfig.getConfig(); 36 | 37 | var parameters = { 38 | limit: config.itemsPerPage, 39 | sort: 'createdAt DESC', 40 | populate: 'user' 41 | }; 42 | 43 | return LoginHistoryModel.load(parameters); 44 | } 45 | ], 46 | _count: [ 47 | 'LoginHistoryModel', 48 | function resolve(LoginHistoryModel) { 49 | return LoginHistoryModel.count(); 50 | } 51 | ], 52 | _statsBrowser: [ 53 | 'LoginHistoryModel', 54 | function resolve(LoginHistoryModel) { 55 | return LoginHistoryModel.statistics('Browser'); 56 | } 57 | ], 58 | _statsOS: [ 59 | 'LoginHistoryModel', 60 | function resolve(LoginHistoryModel) { 61 | return LoginHistoryModel.statistics('OS'); 62 | } 63 | ], 64 | _statsUser: [ 65 | 'LoginHistoryModel', 66 | function resolve(LoginHistoryModel) { 67 | return LoginHistoryModel.statistics('User'); 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | }) 74 | ; 75 | } 76 | ]) 77 | ; 78 | }()); 79 | -------------------------------------------------------------------------------- /src/app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Frontend application definition. 3 | * 4 | * This is the main file for the 'Frontend' application. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | // Create frontend module and specify dependencies for that 10 | angular.module('frontend', [ 11 | 'frontend-templates', 12 | 'frontend.core', 13 | 'frontend.examples', 14 | 'frontend.admin' 15 | ]); 16 | 17 | /** 18 | * Configuration for frontend application, this contains following main sections: 19 | * 20 | * 1) Configure $httpProvider and $sailsSocketProvider 21 | * 2) Set necessary HTTP and Socket interceptor(s) 22 | * 3) Turn on HTML5 mode on application routes 23 | * 4) Set up application routes 24 | */ 25 | angular.module('frontend') 26 | .config([ 27 | '$stateProvider', '$locationProvider', '$urlRouterProvider', '$httpProvider', '$sailsSocketProvider', 28 | '$tooltipProvider', 'cfpLoadingBarProvider', 29 | 'toastrConfig', 30 | 'AccessLevels', 31 | function config( 32 | $stateProvider, $locationProvider, $urlRouterProvider, $httpProvider, $sailsSocketProvider, 33 | $tooltipProvider, cfpLoadingBarProvider, 34 | toastrConfig, 35 | AccessLevels 36 | ) { 37 | $httpProvider.defaults.useXDomain = true; 38 | 39 | delete $httpProvider.defaults.headers.common['X-Requested-With']; 40 | 41 | // Add interceptors for $httpProvider and $sailsSocketProvider 42 | $httpProvider.interceptors.push('AuthInterceptor'); 43 | $httpProvider.interceptors.push('ErrorInterceptor'); 44 | 45 | // Iterate $httpProvider interceptors and add those to $sailsSocketProvider 46 | angular.forEach($httpProvider.interceptors, function iterator(interceptor) { 47 | $sailsSocketProvider.interceptors.push(interceptor); 48 | }); 49 | 50 | // Set tooltip options 51 | $tooltipProvider.options({ 52 | appendToBody: true 53 | }); 54 | 55 | // Disable spinner from cfpLoadingBar 56 | cfpLoadingBarProvider.includeSpinner = false; 57 | cfpLoadingBarProvider.latencyThreshold = 200; 58 | 59 | // Extend default toastr configuration with application specified configuration 60 | angular.extend( 61 | toastrConfig, 62 | { 63 | allowHtml: true, 64 | closeButton: true, 65 | extendedTimeOut: 3000 66 | } 67 | ); 68 | 69 | // Yeah we wanna to use HTML5 urls! 70 | $locationProvider 71 | .html5Mode({ 72 | enabled: true, 73 | requireBase: false 74 | }) 75 | .hashPrefix('!') 76 | ; 77 | 78 | // Routes that needs authenticated user 79 | $stateProvider 80 | .state('profile', { 81 | abstract: true, 82 | template: '', 83 | data: { 84 | access: AccessLevels.user 85 | } 86 | }) 87 | .state('profile.edit', { 88 | url: '/profile', 89 | templateUrl: '/frontend/profile/profile.html', 90 | controller: 'ProfileController' 91 | }) 92 | ; 93 | 94 | // Main state provider for frontend application 95 | $stateProvider 96 | .state('frontend', { 97 | abstract: true, 98 | views: { 99 | header: { 100 | templateUrl: '/frontend/core/layout/partials/header.html', 101 | controller: 'HeaderController' 102 | }, 103 | footer: { 104 | templateUrl: '/frontend/core/layout/partials/footer.html', 105 | controller: 'FooterController' 106 | } 107 | } 108 | }) 109 | ; 110 | 111 | // For any unmatched url, redirect to /about 112 | $urlRouterProvider.otherwise('/about'); 113 | } 114 | ]) 115 | ; 116 | 117 | /** 118 | * Frontend application run hook configuration. This will attach auth status 119 | * check whenever application changes URL states. 120 | */ 121 | angular.module('frontend') 122 | .run([ 123 | '$rootScope', '$state', '$injector', 124 | 'editableOptions', 125 | 'AuthService', 126 | function run( 127 | $rootScope, $state, $injector, 128 | editableOptions, 129 | AuthService 130 | ) { 131 | // Set usage of Bootstrap 3 CSS with angular-xeditable 132 | editableOptions.theme = 'bs3'; 133 | 134 | /** 135 | * Route state change start event, this is needed for following: 136 | * 1) Check if user is authenticated to access page, and if not redirect user back to login page 137 | */ 138 | $rootScope.$on('$stateChangeStart', function stateChangeStart(event, toState) { 139 | if (!AuthService.authorize(toState.data.access)) { 140 | event.preventDefault(); 141 | 142 | $state.go('auth.login'); 143 | } 144 | }); 145 | 146 | // Check for state change errors. 147 | $rootScope.$on('$stateChangeError', function stateChangeError(event, toState, toParams, fromState, fromParams, error) { 148 | event.preventDefault(); 149 | 150 | $injector.get('MessageService') 151 | .error('Error loading the page'); 152 | 153 | $state.get('error').error = { 154 | event: event, 155 | toState: toState, 156 | toParams: toParams, 157 | fromState: fromState, 158 | fromParams: fromParams, 159 | error: error 160 | }; 161 | 162 | return $state.go('error'); 163 | }); 164 | } 165 | ]) 166 | ; 167 | }()); 168 | -------------------------------------------------------------------------------- /src/app/app.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/base'; 2 | -------------------------------------------------------------------------------- /src/app/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarlepp/angular-sailsjs-boilerplate-frontend/e6f0544097c0913b12d2b4cbd01eae34297bbc97/src/app/assets/.gitkeep -------------------------------------------------------------------------------- /src/app/core/auth/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular module for frontend.core.auth component. This component is divided to following logical components: 3 | * 4 | * frontend.core.auth.login 5 | * frontend.core.auth.services 6 | */ 7 | (function() { 8 | 'use strict'; 9 | 10 | // Define frontend.auth module 11 | angular.module('frontend.core.auth', [ 12 | 'frontend.core.auth.login', 13 | 'frontend.core.auth.services' 14 | ]); 15 | 16 | // Module configuration 17 | angular.module('frontend.core.auth') 18 | .config([ 19 | '$stateProvider', 20 | function config($stateProvider) { 21 | $stateProvider 22 | .state('auth', { 23 | abstract: true, 24 | parent: 'frontend', 25 | data: { 26 | access: 1 27 | } 28 | }) 29 | ; 30 | } 31 | ]) 32 | ; 33 | }()); 34 | -------------------------------------------------------------------------------- /src/app/core/auth/login/login-controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular controller definitions for 'frontend.auth.login' module. 3 | * 4 | * Note that this file should only contain controllers and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | /** 10 | * Login controller to handle user's login to application. Controller uses 'Auth' service to make actual HTTP 11 | * request to server and try to authenticate user. 12 | * 13 | * After successfully login Auth service will store user data and JWT token via 'Storage' service where those are 14 | * asked whenever needed in application. 15 | * 16 | * @todo 17 | * 1) different authentication providers 18 | * 2) user registration 19 | */ 20 | angular.module('frontend.core.auth.login') 21 | .controller('LoginController', [ 22 | '$scope', '$state', 23 | 'AuthService', 'FocusOnService', 24 | function controller( 25 | $scope, $state, 26 | AuthService, FocusOnService 27 | ) { 28 | // Already authenticated so redirect back to books list 29 | if (AuthService.isAuthenticated()) { 30 | $state.go('examples.books'); 31 | } 32 | 33 | // Scope function to perform actual login request to server 34 | $scope.login = function login() { 35 | AuthService 36 | .login($scope.credentials) 37 | .then( 38 | function successCallback() { 39 | $state.go('examples.books'); 40 | }, 41 | function errorCallback() { 42 | _reset(); 43 | } 44 | ) 45 | ; 46 | }; 47 | 48 | /** 49 | * Private helper function to reset credentials and set focus to username input. 50 | * 51 | * @private 52 | */ 53 | function _reset() { 54 | FocusOnService.focus('username'); 55 | 56 | // Initialize credentials 57 | $scope.credentials = { 58 | identifier: '', 59 | password: '' 60 | }; 61 | } 62 | 63 | _reset(); 64 | } 65 | ]) 66 | ; 67 | }()); 68 | -------------------------------------------------------------------------------- /src/app/core/auth/login/login.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Please login

5 | 6 |
9 | 13 |
14 | 15 |
18 | 21 |
22 | 23 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/core/auth/login/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Messages component which is divided to following logical components: 3 | * 4 | * Controllers 5 | * 6 | * All of these are wrapped to 'frontend.auth.login' angular module. 7 | */ 8 | (function() { 9 | 'use strict'; 10 | 11 | // Define frontend.auth.login angular module 12 | angular.module('frontend.core.auth.login', []); 13 | 14 | // Module configuration 15 | angular.module('frontend.core.auth.login') 16 | .config([ 17 | '$stateProvider', 18 | function config($stateProvider) { 19 | $stateProvider 20 | // Login 21 | .state('auth.login', { 22 | url: '/login', 23 | data: { 24 | access: 0 25 | }, 26 | views: { 27 | 'content@': { 28 | templateUrl: '/frontend/core/auth/login/login.html', 29 | controller: 'LoginController' 30 | } 31 | } 32 | }) 33 | ; 34 | } 35 | ]) 36 | ; 37 | }()); 38 | -------------------------------------------------------------------------------- /src/app/core/auth/login/login.scss: -------------------------------------------------------------------------------- 1 | .form-login { 2 | margin: 0 auto; 3 | max-width: 250px; 4 | padding: 15px; 5 | 6 | .title { 7 | font-size: 26px; 8 | text-align: center; 9 | } 10 | 11 | .checkbox { 12 | font-weight: normal; 13 | 14 | input { 15 | float: none; 16 | position: relative; 17 | top: 1px; 18 | } 19 | } 20 | 21 | .form-group { 22 | margin-bottom: 0; 23 | } 24 | 25 | .form-control { 26 | text-align: center; 27 | 28 | &:focus { 29 | z-index: 2; 30 | } 31 | } 32 | 33 | .username { 34 | border-bottom-left-radius: 0; 35 | border-bottom-right-radius: 0; 36 | } 37 | 38 | .password { 39 | border-radius: 0; 40 | } 41 | 42 | .btn { 43 | border-top-left-radius: 0; 44 | border-top-right-radius: 0; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/core/auth/services/AuthService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthService service which is used to authenticate users with backend server and provide simple 3 | * methods to check if user is authenticated or not. 4 | * 5 | * Within successfully login process service will store user data and JWT token to ngStorage where 6 | * those are accessible in the application. 7 | * 8 | * This service provides following methods: 9 | * 10 | * AuthService.authorize(access) 11 | * AuthService.isAuthenticated() 12 | * AuthService.login(credentials) 13 | * AuthService.logout() 14 | * 15 | * You can use this service fairly easy on your controllers and views if you like to. It's 16 | * recommend that you use this service with 'UserService' service in your controllers and 17 | * views. 18 | * 19 | * Usage example in controller: 20 | * 21 | * angular 22 | * .module('app') 23 | * .controller('SomeController', [ 24 | * '$scope', 'AuthService', 'UserService', 25 | * function ($scope, AuthService, UserService) { 26 | * $scope.auth = AuthService; 27 | * $scope.user = UserService.user; 28 | * } 29 | * ]) 30 | * ; 31 | * 32 | * Usage example in view: 33 | * 34 | *
35 | * Hello, {{user().email}} 36 | *
37 | * 38 | * Happy coding! 39 | * 40 | * @todo Revoke method? 41 | * @todo Text localizations? 42 | */ 43 | (function() { 44 | 'use strict'; 45 | 46 | angular.module('frontend.core.auth.services') 47 | .factory('AuthService', [ 48 | '$http', '$state', '$localStorage', 49 | 'AccessLevels', 'BackendConfig', 'MessageService', 50 | function factory( 51 | $http, $state, $localStorage, 52 | AccessLevels, BackendConfig, MessageService 53 | ) { 54 | return { 55 | /** 56 | * Method to authorize current user with given access level in application. 57 | * 58 | * @param {Number} accessLevel Access level to check 59 | * 60 | * @returns {Boolean} 61 | */ 62 | authorize: function authorize(accessLevel) { 63 | if (accessLevel === AccessLevels.user) { 64 | return this.isAuthenticated(); 65 | } else if (accessLevel === AccessLevels.admin) { 66 | return this.isAuthenticated() && Boolean($localStorage.credentials.user.admin); 67 | } else { 68 | return accessLevel === AccessLevels.anon; 69 | } 70 | }, 71 | 72 | /** 73 | * Method to check if current user is authenticated or not. This will just 74 | * simply call 'Storage' service 'get' method and returns it results. 75 | * 76 | * @returns {Boolean} 77 | */ 78 | isAuthenticated: function isAuthenticated() { 79 | return Boolean($localStorage.credentials); 80 | }, 81 | 82 | /** 83 | * Method make login request to backend server. Successfully response from 84 | * server contains user data and JWT token as in JSON object. After successful 85 | * authentication method will store user data and JWT token to local storage 86 | * where those can be used. 87 | * 88 | * @param {*} credentials 89 | * 90 | * @returns {*|Promise} 91 | */ 92 | login: function login(credentials) { 93 | return $http 94 | .post(BackendConfig.url + '/login', credentials, {withCredentials: true}) 95 | .then( 96 | function(response) { 97 | MessageService.success('You have been logged in.'); 98 | 99 | $localStorage.credentials = response.data; 100 | } 101 | ) 102 | ; 103 | }, 104 | 105 | /** 106 | * The backend doesn't care about actual user logout, just delete the token 107 | * and you're good to go. 108 | * 109 | * Question still: Should we make logout process to backend side? 110 | */ 111 | logout: function logout() { 112 | $localStorage.$reset(); 113 | 114 | MessageService.success('You have been logged out.'); 115 | 116 | $state.go('auth.login'); 117 | } 118 | }; 119 | } 120 | ]) 121 | ; 122 | }()); 123 | -------------------------------------------------------------------------------- /src/app/core/auth/services/UserService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Current user data service within this you can access to currently signed in user data. 3 | * Note that if you wanna be secure about this you have to also use 'Auth' service in your 4 | * views. 5 | * 6 | * Usage example in controller: 7 | * angular 8 | * .module('app') 9 | * .controller('SomeController',[ 10 | * '$scope', 'AuthService', 'UserService', 11 | * function controller($scope, AuthService, UserService) { 12 | * $scope.auth = AuthService; 13 | * $scope.user = UserService.user; 14 | * } 15 | * ]) 16 | * ; 17 | * 18 | * Usage example in view: 19 | *
20 | * Hello, {{user().email}} 21 | *
22 | * 23 | * Happy coding! 24 | */ 25 | (function() { 26 | 'use strict'; 27 | 28 | angular.module('frontend.core.auth.services') 29 | .factory('UserService', [ 30 | '$localStorage', 31 | function factory($localStorage) { 32 | return { 33 | user: function user() { 34 | return $localStorage.credentials ? $localStorage.credentials.user : {}; 35 | } 36 | }; 37 | } 38 | ]) 39 | ; 40 | }()); 41 | -------------------------------------------------------------------------------- /src/app/core/auth/services/services.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.auth.services', []); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app/core/components/FocusOn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic 'focus' component for boilerplate application. Purpose of this component is to provide an easy way to trigger 3 | * focus to specified input whenever needed. 4 | * 5 | * @link https://stackoverflow.com/questions/14833326/how-to-set-focus-on-input-field/18295416#18295416 6 | */ 7 | (function() { 8 | 'use strict'; 9 | 10 | /** 11 | * Directive definition for 'focus' component. This will listen 'focusOn' event on scope, and whenever that's 12 | * fired directive will check if that event is attached to current element. 13 | * 14 | * Usage example: 15 | * 16 | * 17 | * This will attach 'focusOn' event with 'focusMe' parameter to trigger focus of this element. 18 | */ 19 | angular.module('frontend.core.components') 20 | .directive('focusOn', [ 21 | '$timeout', 22 | function directive($timeout) { 23 | /** 24 | * Actual directive return function. 25 | * 26 | * @param {angular.scope} scope Angular scope object. 27 | * @param {angular.element} element jqLite-wrapped element that this directive matches. 28 | */ 29 | return function focusOn(scope, element) { 30 | scope.$on('focusOn', function focusOnEvent(event, identifier) { 31 | if (element.data('focusOn') && identifier === element.data('focusOn')) { 32 | $timeout(function timeout() { 33 | element.focus(); 34 | }, 0, false); 35 | } 36 | }); 37 | }; 38 | } 39 | ]) 40 | ; 41 | 42 | /** 43 | * Service for focus component. This is need for actual element focus events which can be activated from another 44 | * components like controllers and services. 45 | * 46 | * Usage example: 47 | * angular.module('frontend.controllers') 48 | * .controller('MyCtrl', [ 49 | * '$scope', 'FocusOnService', 50 | * function($scope, FocusOnService) { 51 | * focusOnService('focusMe'); 52 | * } 53 | * ]) 54 | * ; 55 | * 56 | * This will trigger focus to input element that has 'data-focus-on' attribute set with value 'focusMe'. 57 | */ 58 | angular.module('frontend.core.components') 59 | .factory('FocusOnService', [ 60 | '$rootScope', '$timeout', 61 | function factory($rootScope, $timeout) { 62 | /** 63 | * Actual functionality for this service. This will just broadcast 'focusOn' event with specified 64 | * identifier, which is catch on 'focus' component directive. 65 | * 66 | * @param {string} identifier Identifier for 'data-focus-on' attribute 67 | */ 68 | return { 69 | 'focus': function focus(identifier) { 70 | $timeout(function timeout() { 71 | $rootScope.$broadcast('focusOn', identifier); 72 | }, 0, false); 73 | } 74 | }; 75 | } 76 | ]) 77 | ; 78 | }()); 79 | -------------------------------------------------------------------------------- /src/app/core/components/components.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.components', []); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app/core/constants/AccessLevels.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Frontend application access level constant definitions. These are used to to restrict access to certain routes in 3 | * application. 4 | * 5 | * Note that actual access check is done by currently signed in user. 6 | */ 7 | (function() { 8 | 'use strict'; 9 | 10 | angular.module('frontend') 11 | .constant('AccessLevels', { 12 | anon: 0, 13 | user: 1, 14 | admin: 2 15 | }) 16 | ; 17 | }()); 18 | -------------------------------------------------------------------------------- /src/app/core/constants/BackendConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Frontend application backend constant definitions. This is something that you must define in your application. 3 | * 4 | * Note that 'BackendConfig.url' is configured in /frontend/config/config.json file and you _must_ change it to match 5 | * your backend API url. 6 | */ 7 | (function() { 8 | 'use strict'; 9 | 10 | angular.module('frontend') 11 | .constant('BackendConfig', { 12 | url: window.io.sails.url 13 | }) 14 | ; 15 | }()); 16 | -------------------------------------------------------------------------------- /src/app/core/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular module for 'core' component. This component is divided to following logical components: 3 | * 4 | * frontend.core.dependencies 5 | * frontend.core.auth 6 | * frontend.core.components 7 | * frontend.core.directives 8 | * frontend.core.error 9 | * frontend.core.filters 10 | * frontend.core.interceptors 11 | * frontend.core.layout 12 | * frontend.core.libraries 13 | * frontend.core.models 14 | * frontend.core.services 15 | */ 16 | (function() { 17 | 'use strict'; 18 | 19 | // Define frontend.core module 20 | angular.module('frontend.core', [ 21 | 'frontend.core.dependencies', // Note that this must be loaded first 22 | 'frontend.core.auth', 23 | 'frontend.core.components', 24 | 'frontend.core.directives', 25 | 'frontend.core.error', 26 | 'frontend.core.filters', 27 | 'frontend.core.interceptors', 28 | 'frontend.core.layout', 29 | 'frontend.core.libraries', 30 | 'frontend.core.models', 31 | 'frontend.core.services' 32 | ]); 33 | }()); 34 | -------------------------------------------------------------------------------- /src/app/core/dependencies/dependencies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic models angular module initialize. This module contains all 3rd party dependencies that application needs to 3 | * actually work. 4 | * 5 | * Also note that this module have to be loaded before any other application modules that have dependencies to these 6 | * "core" modules. 7 | */ 8 | (function() { 9 | 'use strict'; 10 | 11 | angular.module('frontend.core.dependencies', [ 12 | 'angular-loading-bar', 13 | 'ngAnimate', 14 | 'ngSanitize', 15 | 'ngBootbox', 16 | 'ngStorage', 17 | 'ui.router', 18 | 'ui.bootstrap', 19 | 'ui.bootstrap.showErrors', 20 | 'ui.utils', 21 | 'angularMoment', 22 | 'toastr', 23 | 'xeditable', 24 | 'sails.io', 25 | 'highcharts-ng' 26 | ]); 27 | }()); 28 | -------------------------------------------------------------------------------- /src/app/core/directives/ListSearch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Directive to create search component for lists. This is used generally in all lists on application. Basically 3 | * this directive just manipulates given filters and items per page variables. Directive needs three attributes to 4 | * work: 5 | * 1) filters, filter data 6 | * 2) options, items per page options 7 | * 3) items, current items per page value 8 | * 9 | * Passed filters must be in following format: 10 | * $scope.filters = { 11 | * searchWord: '', 12 | * columns: $scope.items 13 | * }; 14 | * 15 | * Where '$scope.items' is array of objects like: 16 | * $scope.items = [ 17 | * { 18 | * title: 'Object', 19 | * column: 'objectName', 20 | * searchable: true, 21 | * sortable: true, 22 | * inSearch: true, 23 | * inTitle: true 24 | * }, 25 | * ]; 26 | * 27 | * Usage example: 28 | * 29 | * 34 | */ 35 | (function() { 36 | 'use strict'; 37 | 38 | angular.module('frontend.core.directives') 39 | .directive('listSearch', function directive() { 40 | return { 41 | restrict: 'E', 42 | scope: { 43 | filters: '=', 44 | items: '=', 45 | options: '=' 46 | }, 47 | replace: true, 48 | templateUrl: '/frontend/core/directives/partials/ListSearch.html', 49 | controller: [ 50 | '$scope', 51 | function controller($scope) { 52 | $scope.id = Math.floor((Math.random() * 6) + 1); 53 | 54 | $scope.inSearch = function inSearch(item) { 55 | return (!angular.isUndefined(item.searchable)) ? item.searchable : false; 56 | }; 57 | } 58 | ] 59 | }; 60 | }) 61 | ; 62 | }()); 63 | -------------------------------------------------------------------------------- /src/app/core/directives/directives.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.directives', []); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app/core/directives/partials/ListSearch.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 | 9 | 30 | 31 | 34 | 35 | 36 |
37 | 38 | 43 |
44 |
45 |
46 | 47 | -------------------------------------------------------------------------------- /src/app/core/error/error-controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular controller definitions for 'frontend.core.error' module. 3 | * 4 | * Note that this file should only contain controllers and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | /** 10 | * Controller for generic error handling. 11 | */ 12 | angular.module('frontend.core.error') 13 | .controller('ErrorController', [ 14 | '$scope', '$state', 15 | '_', 16 | '_error', 17 | function controller( 18 | $scope, $state, 19 | _, 20 | _error 21 | ) { 22 | if (_.isUndefined(_error)) { 23 | return $state.go('auth.login'); 24 | } 25 | 26 | $scope.error = _error; 27 | 28 | // Helper function to change current state to previous one 29 | $scope.goToPrevious = function goToPrevious() { 30 | $state.go($scope.error.fromState.name, $scope.error.fromParams); 31 | }; 32 | } 33 | ]) 34 | ; 35 | }()); 36 | -------------------------------------------------------------------------------- /src/app/core/error/error.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.error', []); 6 | 7 | // Module configuration 8 | angular.module('frontend.core.error') 9 | .config([ 10 | '$stateProvider', 11 | function config($stateProvider) { 12 | $stateProvider 13 | .state('error', { 14 | parent: 'frontend', 15 | url: '/error', 16 | data: { 17 | access: 0 18 | }, 19 | views: { 20 | 'content@': { 21 | templateUrl: '/frontend/core/error/partials/error.html', 22 | controller: 'ErrorController', 23 | resolve: { 24 | _error: function resolve() { 25 | return this.self.error; 26 | } 27 | } 28 | } 29 | } 30 | }) 31 | ; 32 | } 33 | ]) 34 | ; 35 | }()); 36 | -------------------------------------------------------------------------------- /src/app/core/error/partials/error.html: -------------------------------------------------------------------------------- 1 |

Error occurred

2 | 3 |

4 | Damn gerbils have stopped running again! Someone has been dispatched to poke them with a sharp stick. 5 |

6 | 7 | 12 | 13 |

14 | 17 | Back to previous page 18 | 19 |

-------------------------------------------------------------------------------- /src/app/core/filters/filters.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.filters', []); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app/core/interceptors/AuthInterceptor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Auth interceptor for HTTP and Socket request. This interceptor will add required 3 | * JWT (Json Web Token) token to each requests. That token is validated in server side 4 | * application. 5 | * 6 | * @see http://angular-tips.com/blog/2014/05/json-web-tokens-introduction/ 7 | * @see http://angular-tips.com/blog/2014/05/json-web-tokens-examples/ 8 | */ 9 | (function() { 10 | 'use strict'; 11 | 12 | angular.module('frontend.core.interceptors') 13 | .factory('AuthInterceptor', [ 14 | '$q', '$injector', '$localStorage', 15 | function( 16 | $q, $injector, $localStorage 17 | ) { 18 | return { 19 | /** 20 | * Interceptor method for $http requests. Main purpose of this method is to add JWT token 21 | * to every request that application does. 22 | * 23 | * @param {*} config HTTP request configuration 24 | * 25 | * @returns {*} 26 | */ 27 | request: function requestCallback(config) { 28 | var token; 29 | 30 | // Yeah we have some user data on local storage 31 | if ($localStorage.credentials) { 32 | token = $localStorage.credentials.token; 33 | } 34 | 35 | // Yeah we have a token 36 | if (token) { 37 | if (!config.data) { 38 | config.data = {}; 39 | } 40 | 41 | /** 42 | * Set token to actual data and headers. Note that we need bot ways because of socket cannot modify 43 | * headers anyway. These values are cleaned up in backend side policy (middleware). 44 | */ 45 | config.data.token = token; 46 | config.headers.authorization = 'Bearer ' + token; 47 | } 48 | 49 | return config; 50 | }, 51 | 52 | /** 53 | * Interceptor method that is triggered whenever response error occurs on $http requests. 54 | * 55 | * @param {*} response 56 | * 57 | * @returns {*|Promise} 58 | */ 59 | responseError: function responseErrorCallback(response) { 60 | if (response.status === 401) { 61 | $localStorage.$reset(); 62 | 63 | $injector.get('$state').go('auth.login'); 64 | } 65 | 66 | return $q.reject(response); 67 | } 68 | }; 69 | } 70 | ]) 71 | ; 72 | }()); 73 | -------------------------------------------------------------------------------- /src/app/core/interceptors/ErrorInterceptor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Interceptor for $http and $sailSocket request to handle possible errors and show 3 | * that error to user automatically. Message is shown by application 'Message' service 4 | * which uses noty library. 5 | * 6 | * @todo Add option to skip showing automatic error message 7 | */ 8 | (function() { 9 | 'use strict'; 10 | 11 | angular.module('frontend.core.interceptors') 12 | .factory('ErrorInterceptor', [ 13 | '$q', '$injector', 14 | function($q, $injector) { 15 | return { 16 | /** 17 | * Interceptor method which is triggered whenever response occurs on $http queries. Note 18 | * that this has some sails.js specified hack for errors that returns HTTP 200 status. 19 | * 20 | * This is maybe sails.js bug, but I'm not sure of that. 21 | * 22 | * @param {*} response 23 | * 24 | * @returns {*|Promise} 25 | */ 26 | response: function responseCallback(response) { 27 | if (response.data.error && 28 | response.data.status && 29 | response.data.status !== 200 30 | ) { 31 | return $q.reject(response); 32 | } else { 33 | return response || $q.when(response); 34 | } 35 | }, 36 | 37 | /** 38 | * Interceptor method that is triggered whenever response error occurs on $http requests. 39 | * 40 | * @param {*} response 41 | * 42 | * @returns {*|Promise} 43 | */ 44 | responseError: function responseErrorCallback(response) { 45 | var message = ''; 46 | 47 | if (response.data && response.data.error) { 48 | message = response.data.error; 49 | } else if (response.data && response.data.message) { 50 | message = response.data.message; 51 | } else { 52 | if (typeof response.data === 'string') { 53 | message = response.data; 54 | } else if (response.statusText) { 55 | message = response.statusText; 56 | } else { 57 | message = $injector.get('HttpStatusService').getStatusCodeText(response.status); 58 | } 59 | 60 | message = message + ' (HTTP status ' + response.status + ')'; 61 | } 62 | 63 | if (message) { 64 | $injector.get('MessageService').error(message); 65 | } 66 | 67 | return $q.reject(response); 68 | } 69 | }; 70 | } 71 | ]) 72 | ; 73 | }()); 74 | -------------------------------------------------------------------------------- /src/app/core/interceptors/interceptors.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.interceptors', []); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app/core/layout/layout-controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular controller definitions for 'frontend.core.layout' module. 3 | * 4 | * Note that this file should only contain controllers and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | /** 10 | * Generic header controller for application layout. this contains all necessary logic which is used on application 11 | * header section. Basically this contains following: 12 | * 13 | * 1) Main navigation 14 | * 2) Login / Logout 15 | * 3) Profile 16 | */ 17 | angular.module('frontend.core.layout') 18 | .controller('HeaderController', [ 19 | '$scope', '$state', 20 | 'HeaderNavigationItems', 21 | 'UserService', 'AuthService', 22 | function controller( 23 | $scope, $state, 24 | HeaderNavigationItems, 25 | UserService, AuthService 26 | ) { 27 | $scope.user = UserService.user; 28 | $scope.auth = AuthService; 29 | $scope.navigationItems = HeaderNavigationItems; 30 | 31 | /** 32 | * Helper function to determine if menu item needs 'not-active' class or not. This is basically 33 | * special case because of 'examples.about' state. 34 | * 35 | * @param {layout.menuItem} item Menu item object 36 | * 37 | * @returns {boolean} 38 | */ 39 | $scope.isNotActive = function isNotActive(item) { 40 | return !!(item.state === 'examples' && $state.current.name === 'examples.about'); 41 | }; 42 | 43 | /** 44 | * Helper function to determine if specified menu item needs 'active' class or not. This is needed 45 | * because of reload of page, in this case top level navigation items are not activated without 46 | * this helper. 47 | * 48 | * @param {layout.menuItem} item Menu item object 49 | * 50 | * @returns {boolean} 51 | */ 52 | $scope.isActive = function isActive(item) { 53 | var bits = $state.current.name.toString().split('.'); 54 | 55 | return !!( 56 | (item.state === $state.current.name) || 57 | (item.state === bits[0] && $state.current.name !== 'examples.about') 58 | ); 59 | }; 60 | 61 | // Simple helper function which triggers user logout action. 62 | $scope.logout = function logout() { 63 | AuthService.logout(); 64 | }; 65 | } 66 | ]) 67 | ; 68 | 69 | /** 70 | * Generic footer controller for application layout. This contains all necessary logic which is used on application 71 | * footer section. Basically this contains following: 72 | * 73 | * 1) Generic links 74 | * 2) Version info parsing (back- and frontend) 75 | */ 76 | angular.module('frontend.core.layout') 77 | .controller('FooterController', [ 78 | function controller() { 79 | // TODO: add version info parsing 80 | } 81 | ]) 82 | ; 83 | 84 | /** 85 | * Generic navigation controller for application layout. This contains all necessary logic for pages sub-navigation 86 | * section. Basically this handles following: 87 | * 88 | * 1) Sub navigation of the page 89 | * 2) Opening of information modal 90 | */ 91 | angular.module('frontend.core.layout') 92 | .controller('NavigationController', [ 93 | '$scope', '$state', '$modal', 94 | '_items', 95 | function controller( 96 | $scope, $state, $modal, 97 | _items 98 | ) { 99 | $scope.navigationItems = _items; 100 | 101 | // Helper function to open information modal about current GUI. 102 | $scope.openInformation = function openInformation() { 103 | $modal.open({ 104 | templateUrl: '/frontend/core/layout/partials/help.html', 105 | controller: 'NavigationModalController', 106 | size: 'lg', 107 | resolve: { 108 | '_title': function resolve() { 109 | return $state.current.name.toString(); 110 | }, 111 | '_files': [ 112 | 'NavigationInfoModalFiles', 113 | function resolve(NavigationInfoModalFiles) { 114 | return NavigationInfoModalFiles.get($state.current.name.toString()); 115 | } 116 | ], 117 | '_template': function resolve() { 118 | return $state.current.views['content@'].templateUrl.replace('.html', '-info.html'); 119 | } 120 | } 121 | }); 122 | }; 123 | } 124 | ]) 125 | ; 126 | 127 | /** 128 | * Controller for navigation info modal. This is used to show GUI specified detailed information about how those 129 | * are done (links to sources + generic information / description). 130 | */ 131 | angular.module('frontend.core.layout') 132 | .controller('NavigationModalController', [ 133 | '$scope', '$modalInstance', 134 | 'BackendConfig', 135 | '_title', '_files', '_template', 136 | function( 137 | $scope, $modalInstance, 138 | BackendConfig, 139 | _title, _files, _template 140 | ) { 141 | $scope.title = _title; 142 | $scope.files = _files; 143 | $scope.template = _template; 144 | $scope.backendConfig = BackendConfig; 145 | 146 | // Dismiss function for modal 147 | $scope.dismiss = function dismiss() { 148 | $modalInstance.dismiss(); 149 | }; 150 | } 151 | ]) 152 | ; 153 | }()); 154 | -------------------------------------------------------------------------------- /src/app/core/layout/layout-directives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular directive definitions for 'frontend.core.layout' module. 3 | * 4 | * Note that this file should only contain directives and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | /** 10 | * Directive to build file links to information modal about current GUI. Actual files are passed to this directive 11 | * within modal open function. 12 | */ 13 | angular.module('frontend.core.layout') 14 | .directive('pageInfoFiles', function directive() { 15 | return { 16 | restrict: 'E', 17 | replace: true, 18 | scope: { 19 | 'files': '@' 20 | }, 21 | templateUrl: '/frontend/core/layout/partials/files.html', 22 | controller: [ 23 | '$scope', 24 | function controller($scope) { 25 | try { 26 | $scope.filesJson = angular.fromJson($scope.files); 27 | } catch (error) { 28 | $scope.filesJson = false; 29 | } 30 | 31 | $scope.getTooltip = function getTooltip(item) { 32 | return '
' + item.title + '
' + item.info; 33 | }; 34 | } 35 | ] 36 | }; 37 | }) 38 | ; 39 | }()); 40 | -------------------------------------------------------------------------------- /src/app/core/layout/layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Layout component to wrap all core layout specified stuff together. This component is divided to following logical 3 | * components: 4 | * 5 | * Controllers 6 | * Directives 7 | * Services 8 | * 9 | * All of these are wrapped to 'frontend.core.layout' angular module. 10 | */ 11 | (function() { 12 | 'use strict'; 13 | 14 | angular.module('frontend.core.layout', []); 15 | }()); 16 | -------------------------------------------------------------------------------- /src/app/core/layout/partials/files.html: -------------------------------------------------------------------------------- 1 |
2 |

Files used in this example

3 | 4 |

5 | Below you can see all the actual backend / frontend files which are used to make this example page work. 6 |

7 | 8 |
9 |
12 |
13 | 14 |
    15 |
  • 16 | 21 |
  • 22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/app/core/layout/partials/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/core/layout/partials/header.html: -------------------------------------------------------------------------------- 1 |
2 | 48 |
-------------------------------------------------------------------------------- /src/app/core/layout/partials/help.html: -------------------------------------------------------------------------------- 1 | 12 | 15 | -------------------------------------------------------------------------------- /src/app/core/layout/partials/navigation.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /src/app/core/libraries/LoDash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular service to inject lo-dash library to your controllers. 3 | * 4 | * Usage example in controller: 5 | * 6 | * angular 7 | * .module('app') 8 | * .controller('SomeController', 9 | * [ 10 | * '$scope', '_', 11 | * function ($scope, _) { 12 | * var foo = _.map(data, function(foo) { return foo.bar = 'foobar'; }); 13 | * } 14 | * ] 15 | * ); 16 | * 17 | * With this you can use lo-dash library easily in your controllers. 18 | */ 19 | (function() { 20 | 'use strict'; 21 | 22 | angular.module('frontend.core.libraries') 23 | .factory('_', [ 24 | '$window', 25 | function factory($window) { 26 | return $window._; 27 | } 28 | ]) 29 | ; 30 | }()); 31 | -------------------------------------------------------------------------------- /src/app/core/libraries/libraries.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.libraries', []); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app/core/models/models.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.models', []); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app/core/services/DataService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic data service to interact with Sails.js backend. This will just 3 | * wrap $sailsSocket methods to a single service, that is used from application. 4 | * 5 | * This is needed because we need to make some common url handling for sails 6 | * endpoint. 7 | */ 8 | (function() { 9 | 'use strict'; 10 | 11 | angular.module('frontend.core.services') 12 | .factory('DataService', [ 13 | '$sailsSocket', 14 | '_', 15 | 'BackendConfig', 16 | function factory( 17 | $sailsSocket, 18 | _, 19 | BackendConfig 20 | ) { 21 | /** 22 | * Helper function to get "proper" end point url for sails backend API. 23 | * 24 | * @param {string} endPoint Name of the end point 25 | * @param {number} [identifier] Identifier of endpoint object 26 | * 27 | * @returns {string} 28 | * @private 29 | */ 30 | function _parseEndPointUrl(endPoint, identifier) { 31 | if (!_.isUndefined(identifier)) { 32 | endPoint = endPoint + '/' + identifier; 33 | } 34 | 35 | return BackendConfig.url + '/' + endPoint; 36 | } 37 | 38 | /** 39 | * Helper function to parse used parameters in 'get' and 'count' methods. 40 | * 41 | * @param {{}} parameters Used query parameters 42 | * 43 | * @returns {{params: {}}} 44 | * @private 45 | */ 46 | function _parseParameters(parameters) { 47 | parameters = parameters || {}; 48 | 49 | return {params: parameters}; 50 | } 51 | 52 | return { 53 | /** 54 | * Service method to get count of certain end point objects. 55 | * 56 | * @param {string} endPoint Name of the end point 57 | * @param {{}} parameters Used query parameters 58 | * 59 | * @returns {Promise|*} 60 | */ 61 | count: function count(endPoint, parameters) { 62 | return $sailsSocket 63 | .get(_parseEndPointUrl(endPoint) + '/count/', _parseParameters(parameters)); 64 | }, 65 | 66 | /** 67 | * Service method to get data from certain end point. This will always return a collection 68 | * of data. 69 | * 70 | * @param {string} endPoint Name of the end point 71 | * @param {{}} parameters Used query parameters 72 | * 73 | * @returns {Promise|*} 74 | */ 75 | collection: function collection(endPoint, parameters) { 76 | return $sailsSocket 77 | .get(_parseEndPointUrl(endPoint), _parseParameters(parameters)); 78 | }, 79 | 80 | /** 81 | * Service method to get data from certain end point. This will return just a one 82 | * record as an object. 83 | * 84 | * @param {string} endPoint Name of the end point 85 | * @param {number} identifier Identifier of endpoint object 86 | * @param {{}} parameters Used query parameters 87 | * 88 | * @returns {Promise|*} 89 | */ 90 | fetch: function fetch(endPoint, identifier, parameters) { 91 | return $sailsSocket 92 | .get(_parseEndPointUrl(endPoint, identifier), _parseParameters(parameters)); 93 | }, 94 | 95 | /** 96 | * Service method to create new object to specified end point. 97 | * 98 | * @param {string} endPoint Name of the end point 99 | * @param {{}} data Data to update 100 | * 101 | * @returns {Promise|*} 102 | */ 103 | create: function create(endPoint, data) { 104 | return $sailsSocket 105 | .post(_parseEndPointUrl(endPoint), data); 106 | }, 107 | 108 | /** 109 | * Service method to update specified end point object. 110 | * 111 | * @param {string} endPoint Name of the end point 112 | * @param {number} identifier Identifier of endpoint object 113 | * @param {{}} data Data to update 114 | * 115 | * @returns {Promise|*} 116 | */ 117 | update: function update(endPoint, identifier, data) { 118 | return $sailsSocket 119 | .put(_parseEndPointUrl(endPoint, identifier), data); 120 | }, 121 | 122 | /** 123 | * Service method to delete specified object. 124 | * 125 | * @param {string} endPoint Name of the end point 126 | * @param {number} identifier Identifier of endpoint object 127 | * 128 | * @returns {Promise|*} 129 | */ 130 | delete: function remove(endPoint, identifier) { 131 | return $sailsSocket 132 | .delete(_parseEndPointUrl(endPoint, identifier)); 133 | } 134 | }; 135 | } 136 | ]) 137 | ; 138 | }()); 139 | -------------------------------------------------------------------------------- /src/app/core/services/HttpStatusService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Service to wrap generic HTTP status specified helper methods. Currently this service has 3 | * following methods available: 4 | * 5 | * HttpStatusService.getStatusCodeText(httpStatusCode); 6 | * 7 | * @todo add more these helpers :D 8 | */ 9 | (function() { 10 | 'use strict'; 11 | 12 | angular.module('frontend.core.services') 13 | .factory('HttpStatusService', 14 | function factory() { 15 | return { 16 | /** 17 | * Getter method for HTTP status message by given status code. 18 | * 19 | * @param {Number} statusCode HTTP status code 20 | * 21 | * @returns {String} Status message 22 | */ 23 | getStatusCodeText: function getStatusCodeText(statusCode) { 24 | var output = ''; 25 | 26 | switch (parseInt(statusCode.toString(), 10)) { 27 | // 1xx Informational 28 | case 100: 29 | output = 'Continue'; 30 | break; 31 | case 101: 32 | output = 'Switching Protocols'; 33 | break; 34 | case 102: 35 | output = 'Processing (WebDAV; RFC 2518)'; 36 | break; 37 | // 2xx Success 38 | case 200: 39 | output = 'OK'; 40 | break; 41 | case 201: 42 | output = 'Created'; 43 | break; 44 | case 202: 45 | output = 'Accepted'; 46 | break; 47 | case 203: 48 | output = 'Non-Authoritative Information (since HTTP/1.1)'; 49 | break; 50 | case 204: 51 | output = 'No Content'; 52 | break; 53 | case 205: 54 | output = 'Reset Content'; 55 | break; 56 | case 206: 57 | output = 'Partial Content'; 58 | break; 59 | case 207: 60 | output = 'Multi-Status (WebDAV; RFC 4918)'; 61 | break; 62 | case 208: 63 | output = 'Already Reported (WebDAV; RFC 5842)'; 64 | break; 65 | case 226: 66 | output = 'IM Used (RFC 3229)'; 67 | break; 68 | // 3xx Redirection 69 | case 300: 70 | output = 'Multiple Choices'; 71 | break; 72 | case 301: 73 | output = 'Moved Permanently'; 74 | break; 75 | case 302: 76 | output = 'Found'; 77 | break; 78 | case 303: 79 | output = 'See Other'; 80 | break; 81 | case 304: 82 | output = 'Not Modified'; 83 | break; 84 | case 305: 85 | output = 'Use Proxy'; 86 | break; 87 | case 306: 88 | output = 'Switch Proxy'; 89 | break; 90 | case 307: 91 | output = 'Temporary Redirect'; 92 | break; 93 | case 308: 94 | output = 'Permanent Redirect (Experimental RFC; RFC 7238)'; 95 | break; 96 | // 4xx Client Error 97 | case 400: 98 | output = 'Bad Request'; 99 | break; 100 | case 401: 101 | output = 'Unauthorized'; 102 | break; 103 | case 402: 104 | output = 'Payment Required'; 105 | break; 106 | case 403: 107 | output = 'Forbidden'; 108 | break; 109 | case 404: 110 | output = 'Not Found'; 111 | break; 112 | case 405: 113 | output = 'Method Not Allowed'; 114 | break; 115 | case 406: 116 | output = 'Not Acceptable'; 117 | break; 118 | case 407: 119 | output = 'Proxy Authentication Required'; 120 | break; 121 | case 408: 122 | output = 'Request Timeout'; 123 | break; 124 | case 409: 125 | output = 'Conflict'; 126 | break; 127 | case 410: 128 | output = 'Gone'; 129 | break; 130 | case 411: 131 | output = 'Length Required'; 132 | break; 133 | case 412: 134 | output = 'Precondition Failed'; 135 | break; 136 | case 413: 137 | output = 'Request Entity Too Large'; 138 | break; 139 | case 414: 140 | output = 'Request-URI Too Long'; 141 | break; 142 | case 415: 143 | output = 'Unsupported Media Type'; 144 | break; 145 | case 416: 146 | output = 'Requested Range Not Satisfiable'; 147 | break; 148 | case 417: 149 | output = 'Expectation Failed'; 150 | break; 151 | case 418: 152 | output = 'I\'m a teapot (RFC 2324)'; 153 | break; 154 | case 419: 155 | output = 'Authentication Timeout (not in RFC 2616)'; 156 | break; 157 | case 420: 158 | output = 'Method Failure (Spring Framework) / Enhance Your Calm (Twitter)'; 159 | break; 160 | case 422: 161 | output = 'Unprocessable Entity (WebDAV; RFC 4918)'; 162 | break; 163 | case 423: 164 | output = 'Locked (WebDAV; RFC 4918)'; 165 | break; 166 | case 424: 167 | output = 'Failed Dependency (WebDAV; RFC 4918)'; 168 | break; 169 | case 426: 170 | output = 'Upgrade Required'; 171 | break; 172 | case 428: 173 | output = 'Precondition Required (RFC 6585)'; 174 | break; 175 | case 429: 176 | output = 'Too Many Requests (RFC 6585)'; 177 | break; 178 | case 431: 179 | output = 'Request Header Fields Too Large (RFC 6585)'; 180 | break; 181 | case 440: 182 | output = 'Login Timeout (Microsoft)'; 183 | break; 184 | case 444: 185 | output = 'No Response (Nginx)'; 186 | break; 187 | case 449: 188 | output = 'Retry With (Microsoft)'; 189 | break; 190 | case 450: 191 | output = 'Blocked by Windows Parental Controls (Microsoft)'; 192 | break; 193 | case 451: 194 | output = 'Unavailable For Legal Reasons (Internet draft) / Redirect (Microsoft)'; 195 | break; 196 | case 494: 197 | output = 'Request Header Too Large (Nginx)'; 198 | break; 199 | case 495: 200 | output = 'Cert Error (Nginx)'; 201 | break; 202 | case 496: 203 | output = 'No Cert (Nginx)'; 204 | break; 205 | case 497: 206 | output = 'HTTP to HTTPS (Nginx)'; 207 | break; 208 | case 498: 209 | output = 'Token expired/invalid (Esri)'; 210 | break; 211 | case 499: 212 | output = 'Client Closed Request (Nginx) / Token required (Esri)'; 213 | break; 214 | // 5xx Server Error 215 | case 500: 216 | output = 'Internal Server Error'; 217 | break; 218 | case 501: 219 | output = 'Not Implemented'; 220 | break; 221 | case 502: 222 | output = 'Bad Gateway'; 223 | break; 224 | case 503: 225 | output = 'Service Unavailable'; 226 | break; 227 | case 504: 228 | output = 'Gateway Timeout'; 229 | break; 230 | case 505: 231 | output = 'HTTP Version Not Supported'; 232 | break; 233 | case 506: 234 | output = 'Variant Also Negotiates (RFC 2295)'; 235 | break; 236 | case 507: 237 | output = 'Insufficient Storage (WebDAV; RFC 4918)'; 238 | break; 239 | case 508: 240 | output = 'Loop Detected (WebDAV; RFC 5842)'; 241 | break; 242 | case 509: 243 | output = 'Bandwidth Limit Exceeded (Apache bw/limited extension)'; 244 | break; 245 | case 510: 246 | output = 'Not Extended (RFC 2774)'; 247 | break; 248 | case 511: 249 | output = 'Network Authentication Required (RFC 6585)'; 250 | break; 251 | case 520: 252 | output = 'Origin Error (Cloudflare)'; 253 | break; 254 | case 521: 255 | output = 'Web server is down (Cloudflare)'; 256 | break; 257 | case 522: 258 | output = 'Connection timed out (Cloudflare)'; 259 | break; 260 | case 523: 261 | output = 'Proxy Declined Request (Cloudflare)'; 262 | break; 263 | case 524: 264 | output = 'A timeout occurred (Cloudflare)'; 265 | break; 266 | case 598: 267 | output = 'Network read timeout error (Unknown)'; 268 | break; 269 | case 599: 270 | output = 'Network connect timeout error (Unknown)'; 271 | break; 272 | default: 273 | output = 'Unknown HTTP status \'' + statusCode + '\', what is this?'; 274 | break; 275 | } 276 | 277 | return output; 278 | } 279 | }; 280 | } 281 | ) 282 | ; 283 | }()); 284 | -------------------------------------------------------------------------------- /src/app/core/services/ListConfigService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple service to return configuration for generic list. This service contains only 3 | * getter methods that all list views uses in Boilerplate frontend application. 4 | * 5 | * So generally you change these getter methods and changes are affected to all list 6 | * views on application. 7 | * 8 | * @todo text translations 9 | */ 10 | (function() { 11 | 'use strict'; 12 | 13 | angular.module('frontend.core.services') 14 | .factory('ListConfig', [ 15 | '_', 16 | function factory(_) { 17 | /** 18 | * List title item configuration. 19 | * 20 | * @type {{ 21 | * author: *[], 22 | * book: *[] 23 | * }} 24 | */ 25 | var titleItems = { 26 | author: [ 27 | { 28 | title: 'Author', 29 | column: 'name', 30 | class: 'col-xs-11', 31 | searchable: true, 32 | sortable: true, 33 | inSearch: true, 34 | inTitle: true 35 | }, 36 | { 37 | title: 'Books', 38 | column: false, 39 | class: 'text-right col-xs-1', 40 | searchable: false, 41 | sortable: false, 42 | inSearch: false, 43 | inTitle: true 44 | } 45 | ], 46 | book: [ 47 | { 48 | title: 'Title', 49 | column: 'title', 50 | class: 'col-xs-8', 51 | searchable: true, 52 | sortable: true, 53 | inSearch: true, 54 | inTitle: true 55 | }, 56 | { 57 | title: 'Author', 58 | column: false, 59 | class: 'col-xs-3', 60 | searchable: false, 61 | sortable: false, 62 | inSearch: false, 63 | inTitle: true 64 | }, 65 | { 66 | title: 'Year', 67 | column: 'releaseDate', 68 | class: 'col-xs-1 text-right', 69 | searchable: true, 70 | sortable: true, 71 | inSearch: true, 72 | inTitle: true 73 | } 74 | ], 75 | userlogin: [ 76 | { 77 | title: 'IP-address', 78 | column: 'ip', 79 | class: 'col-xs-2', 80 | searchable: true, 81 | sortable: true, 82 | inSearch: true, 83 | inTitle: true 84 | }, 85 | { 86 | title: 'Browser', 87 | column: 'browser', 88 | class: 'col-xs-2', 89 | searchable: true, 90 | sortable: true, 91 | inSearch: true, 92 | inTitle: true 93 | }, 94 | { 95 | title: 'Operating System', 96 | column: 'os', 97 | class: 'col-xs-2', 98 | searchable: true, 99 | sortable: true, 100 | inSearch: true, 101 | inTitle: true 102 | }, 103 | { 104 | title: 'Username', 105 | column: false, 106 | class: 'col-xs-2', 107 | searchable: false, 108 | sortable: false, 109 | inSearch: false, 110 | inTitle: true 111 | }, 112 | { 113 | title: 'Login time', 114 | column: 'createdAt', 115 | class: 'col-xs-4', 116 | searchable: false, 117 | sortable: true, 118 | inSearch: false, 119 | inTitle: true 120 | } 121 | ] 122 | }; 123 | 124 | return { 125 | /** 126 | * Getter method for list default settings. 127 | * 128 | * @returns {{ 129 | * itemCount: Number, 130 | * items: Array, 131 | * itemsPerPage: Number, 132 | * itemsPerPageOptions: Array, 133 | * currentPage: Number, 134 | * where: {}, 135 | * loading: Boolean, 136 | * loaded: Boolean 137 | * }} 138 | */ 139 | getConfig: function getConfig() { 140 | return { 141 | itemCount: 0, 142 | items: [], 143 | itemsPerPage: 10, 144 | itemsPerPageOptions: [10, 25, 50, 100], 145 | currentPage: 1, 146 | where: {}, 147 | loading: true, 148 | loaded: false 149 | }; 150 | }, 151 | 152 | /** 153 | * Getter method for lists title items. These are defined in the 'titleItems' 154 | * variable. 155 | * 156 | * @param {String} model Name of the model 157 | * 158 | * @returns {Array} 159 | */ 160 | getTitleItems: function getTitleItems(model) { 161 | return _.isUndefined(titleItems[model]) ? [] : titleItems[model]; 162 | } 163 | }; 164 | } 165 | ]) 166 | ; 167 | }()); 168 | -------------------------------------------------------------------------------- /src/app/core/services/MessageService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple service to activate noty2 message to GUI. This service can be used every where in application. Generally 3 | * all $http and $socket queries uses this service to show specified errors to user. 4 | * 5 | * Service can be used as in following examples (assuming that you have inject this service to your controller): 6 | * Message.success(message, [title], [options]); 7 | * Message.error(message, [title], [options]); 8 | * Message.message(message, [title], [options]); 9 | * 10 | * Feel free to be happy and code some awesome stuff! 11 | * 12 | * @todo do we need some queue dismiss? 13 | */ 14 | (function() { 15 | 'use strict'; 16 | 17 | angular.module('frontend.core.services') 18 | .factory('MessageService', [ 19 | 'toastr', '_', 20 | function factory(toastr, _) { 21 | var service = {}; 22 | 23 | /** 24 | * Private helper function to make actual message via toastr component. 25 | * 26 | * @param {string} message Message content 27 | * @param {string} title Message title 28 | * @param {{}} options Message specified options 29 | * @param {{}} defaultOptions Default options for current message type 30 | * @param {string} type Message type 31 | * @private 32 | */ 33 | function _makeMessage(message, title, options, defaultOptions, type) { 34 | title = title || ''; 35 | options = options || {}; 36 | 37 | toastr[type](message, title, _.assign(defaultOptions, options)); 38 | } 39 | 40 | /** 41 | * Method to generate 'success' message. 42 | * 43 | * @param {string} message Message content 44 | * @param {string} [title] Message title 45 | * @param {{}} [options] Message options 46 | */ 47 | service.success = function success(message, title, options) { 48 | var defaultOptions = { 49 | timeOut: 2000 50 | }; 51 | 52 | _makeMessage(message, title, options, defaultOptions, 'success'); 53 | }; 54 | 55 | /** 56 | * Method to generate 'info' message. 57 | * 58 | * @param {string} message Message content 59 | * @param {string} [title] Message title 60 | * @param {{}} [options] Message options 61 | */ 62 | service.info = function error(message, title, options) { 63 | var defaultOptions = { 64 | timeout: 3000 65 | }; 66 | 67 | _makeMessage(message, title, options, defaultOptions, 'info'); 68 | }; 69 | 70 | /** 71 | * Method to generate 'warning' message. 72 | * 73 | * @param {string} message Message content 74 | * @param {string} [title] Message title 75 | * @param {{}} [options] Message options 76 | */ 77 | service.warning = function error(message, title, options) { 78 | var defaultOptions = { 79 | timeout: 3000 80 | }; 81 | 82 | _makeMessage(message, title, options, defaultOptions, 'warning'); 83 | }; 84 | 85 | /** 86 | * Method to generate 'error' message. 87 | * 88 | * @param {string} message Message content 89 | * @param {string} [title] Message title 90 | * @param {{}} [options] Message options 91 | */ 92 | service.error = function error(message, title, options) { 93 | var defaultOptions = { 94 | timeout: 4000 95 | }; 96 | 97 | _makeMessage(message, title, options, defaultOptions, 'error'); 98 | }; 99 | 100 | return service; 101 | } 102 | ]) 103 | ; 104 | }()); 105 | -------------------------------------------------------------------------------- /src/app/core/services/SocketHelperService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple angular service to parse search filters for socket queries. Usage example: 3 | * 4 | * $sailsSocket 5 | * .get("/Book/", { 6 | * params: { 7 | * where: SocketHelperService.getWhere($scope.filters) 8 | * } 9 | * }) 10 | * .then( 11 | * function successCb(response) { 12 | * // Do your data handling here 13 | * } 14 | * function errorCb(response) { 15 | * // Do your error handling here 16 | * } 17 | * ); 18 | * 19 | * @todo add more complex parameter handling 20 | */ 21 | (function() { 22 | 'use strict'; 23 | 24 | angular.module('frontend.core.services') 25 | .factory('SocketHelperService', [ 26 | '_', 27 | function factory(_) { 28 | return { 29 | getWhere: function getWhere(filters, defaults) { 30 | var output = defaults || {}; 31 | 32 | // Determine search columns 33 | var columns = _.filter(filters.columns, function iterator(column) { 34 | return column.inSearch; 35 | }); 36 | 37 | // Determine search words 38 | var words = _.filter(filters.searchWord.split(' ')); 39 | 40 | // We have some search word(s) and column(s) 41 | if (columns.length > 0 && words.length > 0) { 42 | var conditions = []; 43 | 44 | // Iterate each columns 45 | _.forEach(columns, function iteratorColumns(column) { 46 | // Iterate each search word 47 | _.forEach(words, function iteratorWords(word) { 48 | var condition = {}; 49 | 50 | // Create actual condition and push that to main condition 51 | condition[column.column] = {contains: word}; 52 | 53 | conditions.push(condition); 54 | }); 55 | }); 56 | 57 | output = {or: conditions}; 58 | } 59 | 60 | return output; 61 | } 62 | }; 63 | } 64 | ]) 65 | ; 66 | }()); -------------------------------------------------------------------------------- /src/app/core/services/services.js: -------------------------------------------------------------------------------- 1 | // Generic models angular module initialize. 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('frontend.core.services', []); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app/examples/about/about.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular module for frontend.examples.about component. Basically this file contains actual angular module initialize 3 | * and route definitions for this module. 4 | */ 5 | (function() { 6 | 'use strict'; 7 | 8 | // Define frontend.public module 9 | angular.module('frontend.examples.about', []); 10 | 11 | // Module configuration 12 | angular.module('frontend.examples.about') 13 | .config([ 14 | '$stateProvider', 15 | function($stateProvider) { 16 | $stateProvider 17 | .state('examples.about', { 18 | url: '/about', 19 | data: { 20 | access: 0 21 | }, 22 | views: { 23 | 'content@': { 24 | templateUrl: '/frontend/examples/about/about.html' 25 | }, 26 | 'pageNavigation@': false 27 | } 28 | }) 29 | ; 30 | } 31 | ]) 32 | ; 33 | }()); 34 | -------------------------------------------------------------------------------- /src/app/examples/author/add.html: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 |
7 | 10 |
11 | 12 |
13 | 16 |
17 | 18 | 23 |
24 |
25 |
-------------------------------------------------------------------------------- /src/app/examples/author/author-controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular controller definitions for 'frontend.examples.author' module. 3 | * 4 | * Note that this file should only contain controllers and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | // Controller for new author creation. 10 | angular.module('frontend.examples.author') 11 | .controller('AuthorAddController', [ 12 | '$scope', '$state', 13 | 'MessageService', 'AuthorModel', 14 | function controller( 15 | $scope, $state, 16 | MessageService, AuthorModel 17 | ) { 18 | // Initialize author model 19 | $scope.author = { 20 | name: '', 21 | description: '' 22 | }; 23 | 24 | /** 25 | * Scope function to store new author to database. After successfully save user will be redirected 26 | * to view that new created author. 27 | */ 28 | $scope.addAuthor = function addAuthor() { 29 | AuthorModel 30 | .create(angular.copy($scope.author)) 31 | .then( 32 | function onSuccess(result) { 33 | MessageService.success('New author added successfully'); 34 | 35 | $state.go('examples.author', {id: result.data.id}); 36 | } 37 | ) 38 | ; 39 | }; 40 | } 41 | ]) 42 | ; 43 | 44 | // Controller to show single author on GUI. 45 | angular.module('frontend.examples.author') 46 | .controller('AuthorController', [ 47 | '$scope', '$state', 48 | 'UserService', 'MessageService', 49 | 'AuthorModel', 'BookModel', 50 | '_author', '_books', '_booksCount', 51 | function controller( 52 | $scope, $state, 53 | UserService, MessageService, 54 | AuthorModel, BookModel, 55 | _author, _books, _booksCount 56 | ) { 57 | // Set current scope reference to models 58 | AuthorModel.setScope($scope, 'author'); 59 | BookModel.setScope($scope, false, 'books', 'booksCount'); 60 | 61 | // Expose necessary data 62 | $scope.user = UserService.user(); 63 | $scope.author = _author; 64 | $scope.books = _books; 65 | $scope.booksCount = _booksCount.count; 66 | 67 | // Author delete dialog buttons configuration 68 | $scope.confirmButtonsDelete = { 69 | ok: { 70 | label: 'Delete', 71 | className: 'btn-danger', 72 | callback: function callback() { 73 | $scope.deleteAuthor(); 74 | } 75 | }, 76 | cancel: { 77 | label: 'Cancel', 78 | className: 'btn-default pull-left' 79 | } 80 | }; 81 | 82 | // Scope function to save modified author. 83 | $scope.saveAuthor = function saveAuthor() { 84 | var data = angular.copy($scope.author); 85 | 86 | // Make actual data update 87 | AuthorModel 88 | .update(data.id, data) 89 | .then( 90 | function onSuccess() { 91 | MessageService.success('Author "' + $scope.author.name + '" updated successfully'); 92 | } 93 | ) 94 | ; 95 | }; 96 | 97 | // Scope function to delete author 98 | $scope.deleteAuthor = function deleteAuthor() { 99 | AuthorModel 100 | .delete($scope.author.id) 101 | .then( 102 | function onSuccess() { 103 | MessageService.success('Author "' + $scope.author.name + '" deleted successfully'); 104 | 105 | $state.go('examples.authors'); 106 | } 107 | ) 108 | ; 109 | }; 110 | } 111 | ]) 112 | ; 113 | 114 | // Controller which contains all necessary logic for author list GUI on boilerplate application. 115 | angular.module('frontend.examples.author') 116 | .controller('AuthorListController', [ 117 | '$scope', '$q', '$timeout', 118 | '_', 119 | 'ListConfig', 120 | 'SocketHelperService', 'UserService', 'AuthorModel', 121 | '_items', '_count', 122 | function controller( 123 | $scope, $q, $timeout, 124 | _, 125 | ListConfig, 126 | SocketHelperService, UserService, AuthorModel, 127 | _items, _count 128 | ) { 129 | // Set current scope reference to model 130 | AuthorModel.setScope($scope, false, 'items', 'itemCount'); 131 | 132 | // Add default list configuration variable to current scope 133 | $scope = angular.extend($scope, angular.copy(ListConfig.getConfig())); 134 | 135 | // Set initial data 136 | $scope.items = _items; 137 | $scope.itemCount = _count.count; 138 | $scope.user = UserService.user(); 139 | 140 | // Initialize used title items 141 | $scope.titleItems = ListConfig.getTitleItems(AuthorModel.endpoint); 142 | 143 | // Initialize default sort data 144 | $scope.sort = { 145 | column: 'name', 146 | direction: true 147 | }; 148 | 149 | // Initialize filters 150 | $scope.filters = { 151 | searchWord: '', 152 | columns: $scope.titleItems 153 | }; 154 | 155 | // Function to change sort column / direction on list 156 | $scope.changeSort = function changeSort(item) { 157 | var sort = $scope.sort; 158 | 159 | if (sort.column === item.column) { 160 | sort.direction = !sort.direction; 161 | } else { 162 | sort.column = item.column; 163 | sort.direction = true; 164 | } 165 | 166 | _triggerFetchData(); 167 | }; 168 | 169 | /** 170 | * Simple watcher for 'currentPage' scope variable. If this is changed we need to fetch author data 171 | * from server. 172 | */ 173 | $scope.$watch('currentPage', function watcher(valueNew, valueOld) { 174 | if (valueNew !== valueOld) { 175 | _fetchData(); 176 | } 177 | }); 178 | 179 | /** 180 | * Simple watcher for 'itemsPerPage' scope variable. If this is changed we need to fetch author data 181 | * from server. 182 | */ 183 | $scope.$watch('itemsPerPage', function watcher(valueNew, valueOld) { 184 | if (valueNew !== valueOld) { 185 | _triggerFetchData(); 186 | } 187 | }); 188 | 189 | var searchWordTimer; 190 | 191 | /** 192 | * Watcher for 'filter' scope variable, which contains multiple values that we're interested 193 | * within actual GUI. This will trigger new data fetch query to server if following conditions 194 | * have been met: 195 | * 196 | * 1) Actual filter variable is different than old one 197 | * 2) Search word have not been changed in 400ms 198 | * 199 | * If those are ok, then watcher will call 'fetchData' function. 200 | */ 201 | $scope.$watch('filters', function watcher(valueNew, valueOld) { 202 | if (valueNew !== valueOld) { 203 | if (searchWordTimer) { 204 | $timeout.cancel(searchWordTimer); 205 | } 206 | 207 | searchWordTimer = $timeout(_triggerFetchData, 400); 208 | } 209 | }, true); 210 | 211 | /** 212 | * Helper function to trigger actual data fetch from backend. This will just check current page 213 | * scope variable and if it is 1 call 'fetchData' function right away. Any other case just set 214 | * 'currentPage' scope variable to 1, which will trigger watcher to fetch data. 215 | * 216 | * @private 217 | */ 218 | function _triggerFetchData() { 219 | if ($scope.currentPage === 1) { 220 | _fetchData(); 221 | } else { 222 | $scope.currentPage = 1; 223 | } 224 | } 225 | 226 | /** 227 | * Helper function to fetch actual data for GUI from backend server with current parameters: 228 | * 1) Current page 229 | * 2) Search word 230 | * 3) Sort order 231 | * 4) Items per page 232 | * 233 | * Actually this function is doing two request to backend: 234 | * 1) Data count by given filter parameters 235 | * 2) Actual data fetch for current page with filter parameters 236 | * 237 | * These are fetched via 'AuthorModel' service with promises. 238 | * 239 | * @private 240 | */ 241 | function _fetchData() { 242 | $scope.loading = true; 243 | 244 | // Common parameters for count and data query 245 | var commonParameters = { 246 | where: SocketHelperService.getWhere($scope.filters) 247 | }; 248 | 249 | // Data query specified parameters 250 | var parameters = { 251 | populate: 'books', 252 | limit: $scope.itemsPerPage, 253 | skip: ($scope.currentPage - 1) * $scope.itemsPerPage, 254 | sort: $scope.sort.column + ' ' + ($scope.sort.direction ? 'ASC' : 'DESC') 255 | }; 256 | 257 | // Fetch data count 258 | var count = AuthorModel 259 | .count(commonParameters) 260 | .then( 261 | function onSuccess(response) { 262 | $scope.itemCount = response.count; 263 | } 264 | ) 265 | ; 266 | 267 | // Fetch actual data 268 | var load = AuthorModel 269 | .load(_.merge({}, commonParameters, parameters)) 270 | .then( 271 | function onSuccess(response) { 272 | $scope.items = response; 273 | } 274 | ) 275 | ; 276 | 277 | // And wrap those all to promise loading 278 | $q 279 | .all([count, load]) 280 | .finally( 281 | function onFinally() { 282 | $scope.loaded = true; 283 | $scope.loading = false; 284 | } 285 | ) 286 | ; 287 | } 288 | } 289 | ]) 290 | ; 291 | }()); 292 | -------------------------------------------------------------------------------- /src/app/examples/author/author-models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular model definitions for 'frontend.examples.author' module. 3 | * 4 | * Note that this file should only contain models and nothing else. Also note that these "models" are just basically 5 | * services that wraps all things together. 6 | */ 7 | (function() { 8 | 'use strict'; 9 | 10 | /** 11 | * Model for Author API, this is used to wrap all Author objects specified actions and data change actions. 12 | */ 13 | angular.module('frontend.examples.author') 14 | .service('AuthorModel', [ 15 | 'DataModel', 16 | function(DataModel) { 17 | return new DataModel('author'); 18 | } 19 | ]) 20 | ; 21 | }()); 22 | -------------------------------------------------------------------------------- /src/app/examples/author/author.html: -------------------------------------------------------------------------------- 1 |
2 |

Requested author not found

3 |
4 | 5 |
6 |
10 |
11 |
12 |

13 | 18 | {{author.name}} 19 | 20 | 21 | 24 | 28 | 29 | 30 | 31 |

32 | 33 |

39 | {{author.description}} 40 |

41 | 42 |
43 |
44 | 49 | 55 | 62 |
63 |
64 |
65 | 66 |
67 |

Books ({{booksCount}})

68 | 69 | 70 | 71 | 74 | 77 | 78 | 79 | 80 | 81 | 84 | 87 | 88 | 89 |
72 | Title 73 | 75 | Release year 76 |
82 | {{book.title}} 83 | 85 | {{book.releaseDate | amDateFormat: 'YYYY'}} 86 |
90 |
91 |
92 |
93 |
-------------------------------------------------------------------------------- /src/app/examples/author/author.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author component to wrap all author specified stuff together. This component is divided to following logical 3 | * components: 4 | * 5 | * Controllers 6 | * Models 7 | * 8 | * All of these are wrapped to 'frontend.examples.author' angular module. 9 | */ 10 | (function() { 11 | 'use strict'; 12 | 13 | // Define frontend.examples.author angular module 14 | angular.module('frontend.examples.author', []); 15 | 16 | // Module configuration 17 | angular.module('frontend.examples.author') 18 | .config([ 19 | '$stateProvider', 20 | function config($stateProvider) { 21 | $stateProvider 22 | // Authors list 23 | .state('examples.authors', { 24 | url: '/examples/authors', 25 | views: { 26 | 'content@': { 27 | templateUrl: '/frontend/examples/author/list.html', 28 | controller: 'AuthorListController', 29 | resolve: { 30 | _items: [ 31 | 'ListConfig', 32 | 'AuthorModel', 33 | function resolve( 34 | ListConfig, 35 | AuthorModel 36 | ) { 37 | var config = ListConfig.getConfig(); 38 | 39 | var parameters = { 40 | populate: 'books', 41 | limit: config.itemsPerPage, 42 | sort: 'name ASC' 43 | }; 44 | 45 | return AuthorModel.load(parameters); 46 | } 47 | ], 48 | _count: [ 49 | 'AuthorModel', 50 | function resolve(AuthorModel) { 51 | return AuthorModel.count(); 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | }) 58 | 59 | // Single author 60 | .state('examples.author', { 61 | url: '/examples/author/:id', 62 | views: { 63 | 'content@': { 64 | templateUrl: '/frontend/examples/author/author.html', 65 | controller: 'AuthorController', 66 | resolve: { 67 | _author: [ 68 | '$stateParams', 69 | 'AuthorModel', 70 | function resolve( 71 | $stateParams, 72 | AuthorModel 73 | ) { 74 | return AuthorModel.fetch($stateParams.id); 75 | } 76 | ], 77 | _books: [ 78 | '$stateParams', 79 | 'BookModel', 80 | function resolve( 81 | $stateParams, 82 | BookModel 83 | ) { 84 | return BookModel.load({author: $stateParams.id}); 85 | } 86 | ], 87 | _booksCount: [ 88 | '$stateParams', 89 | 'BookModel', 90 | function resolve( 91 | $stateParams, 92 | BookModel 93 | ) { 94 | return BookModel.count({author: $stateParams.id}); 95 | } 96 | ] 97 | } 98 | } 99 | } 100 | }) 101 | 102 | // Add new author 103 | .state('examples.author.add', { 104 | url: '/examples/author/add', 105 | data: { 106 | access: 2 107 | }, 108 | views: { 109 | 'content@': { 110 | templateUrl: '/frontend/examples/author/add.html', 111 | controller: 'AuthorAddController' 112 | } 113 | } 114 | }) 115 | ; 116 | } 117 | ]) 118 | ; 119 | }()); 120 | -------------------------------------------------------------------------------- /src/app/examples/author/list-info.html: -------------------------------------------------------------------------------- 1 |

General info

2 | 3 |

4 | This page demonstrates fetching data from the backend via WebSockets and showing it in a simple list. 5 | This example application fetches authors with related book data populated for each 6 | and also covers data pagination, sort, and search functions. The actual data is fetched from the 7 | following endpoint on the backend. 8 |

9 | 10 |
GET {{backendConfig.url}}/author 
11 | 12 |

13 | Note that all data communication to the backend requires a JSON Web Token (JWT) 14 | for authentication to make sure that user is allowed to access the data. 15 |

16 | 17 |

Functions in this example

18 | 19 |
20 |
21 | Data fetch 22 |
23 |
24 | From backend via WebSockets using $sailsSocket service. 25 |
26 | 27 |
28 | Search 29 |
30 |
31 | Specify which columns are used in search and type your search words to see results while you're typing. 32 |
33 | 34 |
35 | Pagination 36 |
37 |
38 | By default, the page displays an example of ten books per page. You can change the number to be 39 | displayed per page. 40 |
41 | 42 |
43 | Sorting data 44 |
45 |
46 | Simply by clicking the column header. This will fetch data from backend again. Unfortunately not from 47 | relation columns... yet. 48 |
49 | 50 |
51 | Live updates 52 |
53 |
54 | GUI is automatically updated if changes are made to the data. 55 |
56 | 57 |
58 | Navigation 59 |
60 |
61 | Access to a single author and book pages simply by clicking the author or book name. 62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /src/app/examples/author/list.html: -------------------------------------------------------------------------------- 1 |

2 | Authors ({{itemCount}}) 3 | 4 | 26 |

27 | 28 | 29 | 30 | 31 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 63 | 64 | 65 |
34 | 39 | 43 | 44 | 48 |
54 | {{author.name}} 55 | {{author.books.length}}
61 | no data found... 62 |
66 | 67 | 75 | -------------------------------------------------------------------------------- /src/app/examples/book/add.html: -------------------------------------------------------------------------------- 1 |
4 |
5 |
6 |
7 | 10 |
11 | 12 |
13 | 16 |
17 | 18 |
19 | 24 |
25 |
26 | 27 |
28 |

Information

29 | 30 | 31 | 32 | 40 | 41 | 42 | 43 | 50 | 51 |
Author 33 | 39 |
Released 44 | 49 |
52 | 53 | 60 |
61 |
62 |
-------------------------------------------------------------------------------- /src/app/examples/book/book-controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular controller definitions for 'frontend.examples.book' module. 3 | * 4 | * Note that this file should only contain controllers and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | // Controller for new book creation. 10 | angular.module('frontend.examples.book') 11 | .controller('BookAddController', [ 12 | '$scope', '$state', 13 | 'MessageService', 14 | 'BookModel', 15 | '_authors', 16 | function controller( 17 | $scope, $state, 18 | MessageService, 19 | BookModel, 20 | _authors 21 | ) { 22 | // Store authors 23 | $scope.authors = _authors; 24 | 25 | // Initialize book model 26 | $scope.book = { 27 | title: '', 28 | description: '', 29 | author: '', 30 | releaseDate: new Date() 31 | }; 32 | 33 | /** 34 | * Scope function to store new book to database. After successfully save user will be redirected 35 | * to view that new created book. 36 | */ 37 | $scope.addBook = function addBook() { 38 | BookModel 39 | .create(angular.copy($scope.book)) 40 | .then( 41 | function onSuccess(result) { 42 | MessageService.success('New book added successfully'); 43 | 44 | $state.go('examples.book', {id: result.data.id}); 45 | } 46 | ) 47 | ; 48 | }; 49 | } 50 | ]) 51 | ; 52 | 53 | // Controller to show single book on GUI. 54 | angular.module('frontend.examples.book') 55 | .controller('BookController', [ 56 | '$scope', '$state', 57 | 'UserService', 'MessageService', 58 | 'BookModel', 'AuthorModel', 59 | '_book', 60 | function controller( 61 | $scope, $state, 62 | UserService, MessageService, 63 | BookModel, AuthorModel, 64 | _book 65 | ) { 66 | // Set current scope reference to model 67 | BookModel.setScope($scope, 'book'); 68 | 69 | // Initialize scope data 70 | $scope.user = UserService.user(); 71 | $scope.book = _book; 72 | $scope.authors = []; 73 | $scope.selectAuthor = _book.author ? _book.author.id : null; 74 | 75 | // Book delete dialog buttons configuration 76 | $scope.confirmButtonsDelete = { 77 | ok: { 78 | label: 'Delete', 79 | className: 'btn-danger', 80 | callback: function callback() { 81 | $scope.deleteBook(); 82 | } 83 | }, 84 | cancel: { 85 | label: 'Cancel', 86 | className: 'btn-default pull-left' 87 | } 88 | }; 89 | 90 | /** 91 | * Scope function to save the modified book. This will send a 92 | * socket request to the backend server with the modified object. 93 | */ 94 | $scope.saveBook = function saveBook() { 95 | var data = angular.copy($scope.book); 96 | 97 | // Set author id to update data 98 | data.author = $scope.selectAuthor; 99 | 100 | // Make actual data update 101 | BookModel 102 | .update(data.id, data) 103 | .then( 104 | function onSuccess() { 105 | MessageService.success('Book "' + $scope.book.title + '" updated successfully'); 106 | } 107 | ) 108 | ; 109 | }; 110 | 111 | /** 112 | * Scope function to delete current book. This will send DELETE query to backend via web socket 113 | * query and after successfully delete redirect user back to book list. 114 | */ 115 | $scope.deleteBook = function deleteBook() { 116 | BookModel 117 | .delete($scope.book.id) 118 | .then( 119 | function onSuccess() { 120 | MessageService.success('Book "' + $scope.book.title + '" deleted successfully'); 121 | 122 | $state.go('examples.books'); 123 | } 124 | ) 125 | ; 126 | }; 127 | 128 | /** 129 | * Scope function to fetch author data when needed, this is triggered whenever user starts to edit 130 | * current book. 131 | * 132 | * @returns {null|promise} 133 | */ 134 | $scope.loadAuthors = function loadAuthors() { 135 | if ($scope.authors.length) { 136 | return null; 137 | } else { 138 | return AuthorModel 139 | .load() 140 | .then( 141 | function onSuccess(data) { 142 | $scope.authors = data; 143 | } 144 | ) 145 | ; 146 | } 147 | }; 148 | } 149 | ]) 150 | ; 151 | 152 | // Controller which contains all necessary logic for book list GUI on boilerplate application. 153 | angular.module('frontend.examples.book') 154 | .controller('BookListController', [ 155 | '$scope', '$q', '$timeout', 156 | '_', 157 | 'ListConfig', 'SocketHelperService', 158 | 'UserService', 'BookModel', 'AuthorModel', 159 | '_items', '_count', '_authors', 160 | function controller( 161 | $scope, $q, $timeout, 162 | _, 163 | ListConfig, SocketHelperService, 164 | UserService, BookModel, AuthorModel, 165 | _items, _count, _authors 166 | ) { 167 | // Set current scope reference to models 168 | BookModel.setScope($scope, false, 'items', 'itemCount'); 169 | AuthorModel.setScope($scope, false, 'authors'); 170 | 171 | // Add default list configuration variable to current scope 172 | $scope = angular.extend($scope, angular.copy(ListConfig.getConfig())); 173 | 174 | // Set initial data 175 | $scope.items = _items; 176 | $scope.itemCount = _count.count; 177 | $scope.authors = _authors; 178 | $scope.user = UserService.user(); 179 | 180 | // Initialize used title items 181 | $scope.titleItems = ListConfig.getTitleItems(BookModel.endpoint); 182 | 183 | // Initialize default sort data 184 | $scope.sort = { 185 | column: 'releaseDate', 186 | direction: false 187 | }; 188 | 189 | // Initialize filters 190 | $scope.filters = { 191 | searchWord: '', 192 | columns: $scope.titleItems 193 | }; 194 | 195 | // Function to change sort column / direction on list 196 | $scope.changeSort = function changeSort(item) { 197 | var sort = $scope.sort; 198 | 199 | if (sort.column === item.column) { 200 | sort.direction = !sort.direction; 201 | } else { 202 | sort.column = item.column; 203 | sort.direction = true; 204 | } 205 | 206 | _triggerFetchData(); 207 | }; 208 | 209 | /** 210 | * Helper function to fetch specified author property. 211 | * 212 | * @param {Number} authorId Author id to search 213 | * @param {String} [property] Property to return, if not given returns whole author object 214 | * @param {String} [defaultValue] Default value if author or property is not founded 215 | * 216 | * @returns {*} 217 | */ 218 | $scope.getAuthor = function getAuthor(authorId, property, defaultValue) { 219 | defaultValue = defaultValue || 'Unknown'; 220 | property = property || true; 221 | 222 | // Find author 223 | var author = _.find($scope.authors, function iterator(author) { 224 | return parseInt(author.id, 10) === parseInt(authorId.toString(), 10); 225 | }); 226 | 227 | return author ? (property === true ? author : author[property]) : defaultValue; 228 | }; 229 | 230 | /** 231 | * Simple watcher for 'currentPage' scope variable. If this is changed we need to fetch book data 232 | * from server. 233 | */ 234 | $scope.$watch('currentPage', function watcher(valueNew, valueOld) { 235 | if (valueNew !== valueOld) { 236 | _fetchData(); 237 | } 238 | }); 239 | 240 | /** 241 | * Simple watcher for 'itemsPerPage' scope variable. If this is changed we need to fetch book data 242 | * from server. 243 | */ 244 | $scope.$watch('itemsPerPage', function watcher(valueNew, valueOld) { 245 | if (valueNew !== valueOld) { 246 | _triggerFetchData(); 247 | } 248 | }); 249 | 250 | var searchWordTimer; 251 | 252 | /** 253 | * Watcher for 'filter' scope variable, which contains multiple values that we're interested 254 | * within actual GUI. This will trigger new data fetch query to server if following conditions 255 | * have been met: 256 | * 257 | * 1) Actual filter variable is different than old one 258 | * 2) Search word have not been changed in 400ms 259 | * 260 | * If those are ok, then watcher will call 'fetchData' function. 261 | */ 262 | $scope.$watch('filters', function watcher(valueNew, valueOld) { 263 | if (valueNew !== valueOld) { 264 | if (searchWordTimer) { 265 | $timeout.cancel(searchWordTimer); 266 | } 267 | 268 | searchWordTimer = $timeout(_triggerFetchData, 400); 269 | } 270 | }, true); 271 | 272 | /** 273 | * Helper function to trigger actual data fetch from backend. This will just check current page 274 | * scope variable and if it is 1 call 'fetchData' function right away. Any other case just set 275 | * 'currentPage' scope variable to 1, which will trigger watcher to fetch data. 276 | * 277 | * @private 278 | */ 279 | function _triggerFetchData() { 280 | if ($scope.currentPage === 1) { 281 | _fetchData(); 282 | } else { 283 | $scope.currentPage = 1; 284 | } 285 | } 286 | 287 | /** 288 | * Helper function to fetch actual data for GUI from backend server with current parameters: 289 | * 1) Current page 290 | * 2) Search word 291 | * 3) Sort order 292 | * 4) Items per page 293 | * 294 | * Actually this function is doing two request to backend: 295 | * 1) Data count by given filter parameters 296 | * 2) Actual data fetch for current page with filter parameters 297 | * 298 | * These are fetched via 'BookModel' service with promises. 299 | * 300 | * @private 301 | */ 302 | function _fetchData() { 303 | $scope.loading = true; 304 | 305 | // Common parameters for count and data query 306 | var commonParameters = { 307 | where: SocketHelperService.getWhere($scope.filters) 308 | }; 309 | 310 | // Data query specified parameters 311 | var parameters = { 312 | limit: $scope.itemsPerPage, 313 | skip: ($scope.currentPage - 1) * $scope.itemsPerPage, 314 | sort: $scope.sort.column + ' ' + ($scope.sort.direction ? 'ASC' : 'DESC') 315 | }; 316 | 317 | // Fetch data count 318 | var count = BookModel 319 | .count(commonParameters) 320 | .then( 321 | function onSuccess(response) { 322 | $scope.itemCount = response.count; 323 | } 324 | ) 325 | ; 326 | 327 | // Fetch actual data 328 | var load = BookModel 329 | .load(_.merge({}, commonParameters, parameters)) 330 | .then( 331 | function onSuccess(response) { 332 | $scope.items = response; 333 | } 334 | ) 335 | ; 336 | 337 | // Load all needed data 338 | $q 339 | .all([count, load]) 340 | .finally( 341 | function onFinally() { 342 | $scope.loaded = true; 343 | $scope.loading = false; 344 | } 345 | ) 346 | ; 347 | } 348 | } 349 | ]) 350 | ; 351 | }()); 352 | -------------------------------------------------------------------------------- /src/app/examples/book/book-models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular model definitions for 'frontend.examples.book' module. 3 | * 4 | * Note that this file should only contain models and nothing else. Also note that these "models" are just basically 5 | * services that wraps all things together. 6 | */ 7 | (function () { 8 | 'use strict'; 9 | 10 | /** 11 | * Model for Book API, this is used to wrap all Book objects specified actions and data change actions. 12 | */ 13 | angular.module('frontend.examples.book') 14 | .factory('BookModel', [ 15 | 'DataModel', 16 | function factory(DataModel) { 17 | return new DataModel('book'); 18 | } 19 | ]) 20 | ; 21 | }()); 22 | -------------------------------------------------------------------------------- /src/app/examples/book/book.html: -------------------------------------------------------------------------------- 1 |
2 |

Requested book not found

3 |
4 | 5 |
6 |
10 |
11 |
12 |

13 | 18 | {{book.title}} 19 | 20 | 21 | 24 | 28 | 29 | 30 | 31 |

32 | 33 |

39 | {{book.description}} 40 |

41 | 42 |
43 |
44 | 49 | 55 | 62 |
63 |
64 |
65 | 66 |
67 |

Information

68 | 69 | 70 | 71 | 83 | 84 | 85 | 86 | 99 | 100 |
Author 72 | 80 | {{book.author.name}} 81 | 82 |
Released 87 |
88 | {{book.releaseDate | amDateFormat: 'YYYY'}} 89 |
90 | 91 |
92 | 97 |
98 |
101 | 102 | 123 |
124 |
125 |
126 |
-------------------------------------------------------------------------------- /src/app/examples/book/book.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Book component to wrap all book specified stuff together. This component is divided to following logical components: 3 | * 4 | * Controllers 5 | * Models 6 | * 7 | * All of these are wrapped to 'frontend.examples.book' angular module. 8 | */ 9 | (function() { 10 | 'use strict'; 11 | 12 | // Define frontend.examples.book angular module 13 | angular.module('frontend.examples.book', []); 14 | 15 | // Module configuration 16 | angular.module('frontend.examples.book') 17 | .config([ 18 | '$stateProvider', 19 | function config($stateProvider) { 20 | $stateProvider 21 | // Book list 22 | .state('examples.books', { 23 | url: '/examples/books', 24 | views: { 25 | 'content@': { 26 | templateUrl: '/frontend/examples/book/list.html', 27 | controller: 'BookListController', 28 | resolve: { 29 | _items: [ 30 | 'ListConfig', 31 | 'BookModel', 32 | function resolve( 33 | ListConfig, 34 | BookModel 35 | ) { 36 | var config = ListConfig.getConfig(); 37 | 38 | var parameters = { 39 | limit: config.itemsPerPage, 40 | sort: 'releaseDate DESC' 41 | }; 42 | 43 | return BookModel.load(parameters); 44 | } 45 | ], 46 | _count: [ 47 | 'BookModel', 48 | function resolve(BookModel) { 49 | return BookModel.count(); 50 | } 51 | ], 52 | _authors: [ 53 | 'AuthorModel', 54 | function resolve(AuthorModel) { 55 | return AuthorModel.load(); 56 | } 57 | ] 58 | } 59 | } 60 | } 61 | }) 62 | 63 | // Single book 64 | .state('examples.book', { 65 | url: '/examples/book/:id', 66 | views: { 67 | 'content@': { 68 | templateUrl: '/frontend/examples/book/book.html', 69 | controller: 'BookController', 70 | resolve: { 71 | _book: [ 72 | '$stateParams', 73 | 'BookModel', 74 | function resolve( 75 | $stateParams, 76 | BookModel 77 | ) { 78 | return BookModel.fetch($stateParams.id, {populate: 'author'}); 79 | } 80 | ] 81 | } 82 | } 83 | } 84 | }) 85 | 86 | // Add new book 87 | .state('examples.book.add', { 88 | url: '/examples/book/add', 89 | data: { 90 | access: 2 91 | }, 92 | views: { 93 | 'content@': { 94 | templateUrl: '/frontend/examples/book/add.html', 95 | controller: 'BookAddController', 96 | resolve: { 97 | _authors: [ 98 | 'AuthorModel', 99 | function resolve(AuthorModel) { 100 | return AuthorModel.load(); 101 | } 102 | ] 103 | } 104 | } 105 | } 106 | }) 107 | ; 108 | } 109 | ]) 110 | ; 111 | }()); 112 | -------------------------------------------------------------------------------- /src/app/examples/book/list-info.html: -------------------------------------------------------------------------------- 1 |

General info

2 | 3 |

4 | This page demonstrates fetching data from the backend via WebSockets and showing it in a simple list. 5 | This example application fetches books with related author data populated for each 6 | and also covers data pagination, sort, and search functions. The actual data is fetched from the 7 | following endpoint on the backend. 8 |

9 | 10 |
GET {{backendConfig.url}}/book 
11 | 12 |

13 | Note that all data communication to the backend requires a JSON Web Token (JWT) 14 | for authentication to make sure that user is allowed to access the data. 15 |

16 | 17 |

Functions in this example

18 | 19 |
20 |
21 | Data fetch 22 |
23 |
24 | From backend via WebSockets using $sailsSocket service. 25 |
26 | 27 |
28 | Search 29 |
30 |
31 | Specify which columns are used in search and type your search words to see results while you're typing. 32 |
33 | 34 |
35 | Pagination 36 |
37 |
38 | By default, the page displays an example of ten books per page. You can change the number to be 39 | displayed per page. 40 |
41 | 42 |
43 | Sorting data 44 |
45 |
46 | Simply by clicking the column header. This will fetch data from backend again. Unfortunately not from 47 | relation columns... yet. 48 |
49 | 50 |
51 | Live updates 52 |
53 |
54 | GUI is automatically updated if changes are made to the data. 55 |
56 | 57 |
58 | Navigation 59 |
60 |
61 | Access to single book and author pages simply by clicking the book or author name. 62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /src/app/examples/book/list.html: -------------------------------------------------------------------------------- 1 |

2 | Books ({{itemCount}}) 3 | 4 | 26 |

27 | 28 | 29 | 30 | 31 | 49 | 50 | 51 | 52 | 55 | 58 | 65 | 66 | 67 | 68 | 69 | 72 | 73 | 74 |
34 | 39 | 43 | 44 | 48 |
56 | {{book.title}} 57 | 59 | 62 | {{getAuthor(book.author, 'name')}} 63 | 64 | {{book.releaseDate | amDateFormat: 'YYYY'}}
70 | no data founded... 71 |
75 | 76 | -------------------------------------------------------------------------------- /src/app/examples/chat/chat-controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular controller definitions for 'frontend.examples.chat' module. 3 | * 4 | * Note that this file should only contain controllers and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | /** 10 | * Main Chat controller which handles the actions on this Chat example page. This controller is fired up whenever 11 | * user enters to following url: 12 | * 13 | * http://{YourServer}:{YourPort}/chat 14 | * 15 | * where 16 | * YourServer = Usually 'localhost', this depends on your setup. 17 | * YourPort = By default 3000 for production and 3001 for development or something else depending on your 18 | * setup. 19 | * 20 | * Controller handles message loading and creating of new messages to backend side. Basically really simple stuff. 21 | * 22 | * @todo 23 | * 1) implement 'enter' and 'leave' status messages to chat 24 | * 2) private messages to another user 25 | * 3) do not load all messages when user enters to chat 26 | * 4) add notification about new chat messages, if user is elsewhere on app 27 | */ 28 | angular.module('frontend.examples.chat') 29 | .controller('ChatController', [ 30 | '$scope', '$timeout', '$localStorage', 31 | 'moment', 32 | 'MessageService', 33 | 'MessageModel', 34 | '_messages', 35 | function controller( 36 | $scope, $timeout, $localStorage, 37 | moment, 38 | MessageService, 39 | MessageModel, 40 | _messages 41 | ) { 42 | // Add loaded messages to scope 43 | $scope.messages = _messages; 44 | 45 | // Get current nick of user 46 | $scope.nick = ($localStorage.chat && $localStorage.chat.nick) ? $localStorage.chat.nick : ''; 47 | 48 | // Initialize message object 49 | $scope.message = { 50 | nick: $scope.nick, 51 | message: '' 52 | }; 53 | 54 | // We have nick set, so load messages 55 | if ($scope.nick && $scope.nick.trim()) { 56 | _scrollBottom(); 57 | } 58 | 59 | // Watcher for actual messages, whenever this is changed we need to scroll chat to bottom 60 | $scope.$watch('messages', function watcher(valueNew) { 61 | if (valueNew) { 62 | _scrollBottom(); 63 | } 64 | }, true); 65 | 66 | // Enter to chat function 67 | $scope.enterToChat = function enterToChat() { 68 | if ($scope.nick && $scope.nick.trim() !== '') { 69 | $scope.message.nick = $scope.nick; 70 | 71 | $localStorage.chat = { 72 | nick: $scope.nick, 73 | time: moment().format() 74 | }; 75 | 76 | _scrollBottom(); 77 | } else { 78 | MessageService.error('Please provide some nick.'); 79 | } 80 | }; 81 | 82 | // Function to leave chat 83 | $scope.leaveChat = function leaveChat() { 84 | $scope.message.nick = ''; 85 | $scope.nick = ''; 86 | $scope.messages = []; 87 | 88 | $localStorage.chat = {}; 89 | }; 90 | 91 | // Function to post a new message to server 92 | $scope.postMessage = function postMessage() { 93 | if ($scope.message.message.trim() !== '') { 94 | MessageModel 95 | .create($scope.message) 96 | .then( 97 | function success() { 98 | $scope.message.message = ''; 99 | 100 | _scrollBottom(); 101 | } 102 | ) 103 | ; 104 | } else { 105 | MessageService.error('Please enter some text to chat.'); 106 | } 107 | }; 108 | 109 | /** 110 | * Helper function to scroll to bottom of the chat 111 | * 112 | * @private 113 | */ 114 | function _scrollBottom() { 115 | $timeout(function timeout() { 116 | document.getElementById('messages').scrollTop = $scope.messages.length * 50; 117 | }); 118 | } 119 | } 120 | ]) 121 | ; 122 | }()); 123 | -------------------------------------------------------------------------------- /src/app/examples/chat/chat-controllers_test.js: -------------------------------------------------------------------------------- 1 | /* jshint strict:false, globalstrict:false */ 2 | /* global describe, it, beforeEach, inject, module */ 3 | describe('ChatController', function() { 4 | var ChatController; 5 | var scope; 6 | var timeout; 7 | var localStorage; 8 | var MessageService; 9 | var MessageModel; 10 | var _messages; 11 | var sandbox; 12 | 13 | beforeEach(module('frontend')); 14 | 15 | beforeEach(inject(function($injector) { 16 | scope = $injector.get('$rootScope'); 17 | timeout = $injector.get('$timeout'); 18 | localStorage = $injector.get('$localStorage'); 19 | MessageService = $injector.get('MessageService'); 20 | MessageModel = $injector.get('MessageModel'); 21 | _messages = []; 22 | sandbox = sinon.sandbox.create(); 23 | 24 | ChatController = function() { 25 | return $injector.get('$controller')('ChatController', { 26 | $scope: scope, 27 | $timeout: timeout, 28 | $localStorage: localStorage, 29 | MessageService: MessageService, 30 | MessageModel: MessageModel, 31 | _messages: _messages 32 | }); 33 | }; 34 | })); 35 | 36 | afterEach(function() { 37 | sandbox.restore(); 38 | }); 39 | 40 | it('should have specified defaults', function() { 41 | ChatController(); 42 | 43 | expect(scope.messages).to.be.an('array'); 44 | expect(scope.nick).equal(''); 45 | expect(scope.message.nick).equal(''); 46 | expect(scope.message.message).equal(''); 47 | }); 48 | 49 | describe('When entering chat', function() { 50 | it('with nick', function() { 51 | ChatController(); 52 | 53 | scope.nick = 'test nick'; 54 | scope.enterToChat(); 55 | 56 | expect(scope.message.nick).equal('test nick'); 57 | }); 58 | 59 | it('without nick', function() { 60 | ChatController(); 61 | 62 | sandbox.stub(MessageService, 'error'); 63 | 64 | scope.nick = ''; 65 | scope.enterToChat(); 66 | 67 | expect(MessageService.error.calledOnce).to.equal(true); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/app/examples/chat/chat-directives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular directive definitions for 'frontend.examples.chat' module. 3 | * 4 | * Note that this file should only contain directives and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | /** 10 | * Directive to resize "chat" screen to take all "possible" space on browser screen. This is just cruel thing to 11 | * do, but it works like a charm. 12 | */ 13 | angular.module('frontend.examples.chat') 14 | .directive('chatScreen', [ 15 | '$timeout', '$window', 16 | function directive($timeout, $window) { 17 | return { 18 | restrict: 'C', 19 | link: function link(scope, element) { 20 | var resize = function resize() { 21 | var totalHeight = angular.element($window).height() - 170; 22 | 23 | angular.element(element).css('height', totalHeight + 'px'); 24 | }; 25 | 26 | angular.element($window).bind('resize', function onEvent() { 27 | resize(); 28 | }); 29 | 30 | resize(); 31 | } 32 | }; 33 | } 34 | ]) 35 | ; 36 | }()); 37 | -------------------------------------------------------------------------------- /src/app/examples/chat/chat-info.html: -------------------------------------------------------------------------------- 1 |

General info

2 | 3 |

4 | This example demonstrates how to use sails.js and web sockets to pass updates 5 | between clients automatically. This simple chat application is perfect 6 | for this demonstration. All communications here are made via web sockets. 7 |

8 | 9 |

Functions in this example

10 | 11 |
12 |
13 | Basic stuff 14 |
15 |
16 | Enter / leave chat. Create new chat message. Message updates to all clients. 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/examples/chat/chat-models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular model definitions for 'frontend.examples.chat' module. 3 | * 4 | * Note that this file should only contain models and nothing else. Also note that these "models" are just basically 5 | * services that wraps all things together. 6 | */ 7 | (function() { 8 | 'use strict'; 9 | 10 | /** 11 | * Model for Message API, this is used to wrap all Message objects specified actions and data change actions. 12 | */ 13 | angular.module('frontend.examples.chat') 14 | .factory('MessageModel', [ 15 | 'DataModel', 16 | function factory(DataModel) { 17 | var model = new DataModel('message'); 18 | 19 | // Custom handler for created objects 20 | model.handlerCreated = function handlerCreated(message){ 21 | this.objects.push(message.data); 22 | }; 23 | 24 | return model; 25 | } 26 | ]) 27 | ; 28 | }()); 29 | -------------------------------------------------------------------------------- /src/app/examples/chat/chat.html: -------------------------------------------------------------------------------- 1 |
4 |

Enter a nick

5 | 6 |
7 | 11 | 12 | 15 | 16 |
17 |
18 | 19 |
22 |
23 |
24 |
    25 |
  • 28 | 29 | [{{message.createdAt | amDateFormat:'YYYY-MM-DD HH:mm:ss'}}] 30 | 31 | 32 | 33 | {{message.nick}} 34 | 35 | 36 |
  • 37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | {{message.nick}} 45 |
46 | 47 | 51 | 52 |
53 | 58 | 59 | 65 |
66 |
67 |
68 |
-------------------------------------------------------------------------------- /src/app/examples/chat/chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Chat component to wrap all book specified stuff together. This component is divided to following logical components: 3 | * 4 | * Controllers 5 | * Directives 6 | * Models 7 | * 8 | * All of these are wrapped to 'frontend.examples.chat' angular module. 9 | */ 10 | (function() { 11 | 'use strict'; 12 | 13 | // Define frontend.examples.chat angular module 14 | angular.module('frontend.examples.chat', []); 15 | 16 | // Module configuration 17 | angular.module('frontend.examples.chat') 18 | .config([ 19 | '$stateProvider', 20 | function config($stateProvider) { 21 | $stateProvider 22 | // Chat 23 | .state('examples.chat', { 24 | url: '/examples/chat', 25 | views: { 26 | 'content@': { 27 | templateUrl: '/frontend/examples/chat/chat.html', 28 | controller: 'ChatController', 29 | resolve: { 30 | _messages: [ 31 | '$localStorage', 32 | 'moment', 33 | 'MessageModel', 34 | function resolve( 35 | $localStorage, 36 | moment, 37 | MessageModel 38 | ) { 39 | var parameters = { 40 | where: { 41 | createdAt: { 42 | '>': ($localStorage.chat && $localStorage.chat.time) ? 43 | $localStorage.chat.time : moment().format() 44 | } 45 | } 46 | }; 47 | 48 | return MessageModel.load(parameters); 49 | } 50 | ] 51 | } 52 | } 53 | } 54 | }) 55 | ; 56 | } 57 | ]) 58 | ; 59 | }()); 60 | -------------------------------------------------------------------------------- /src/app/examples/chat/chat.scss: -------------------------------------------------------------------------------- 1 | $color-border: #dfdfdf; 2 | 3 | .chat { 4 | margin: 0 15px; 5 | padding: 0; 6 | 7 | .messages { 8 | border: 1px solid $color-border; 9 | border-bottom: 0; 10 | border-top: 0; 11 | height: 100vh; 12 | overflow-x: hidden; 13 | overflow-y: scroll; 14 | padding: 0 10px; 15 | 16 | code { 17 | background-color: transparent; 18 | } 19 | 20 | .time { 21 | left: 5px; 22 | position: absolute; 23 | } 24 | 25 | .message { 26 | margin-left: 155px; 27 | } 28 | } 29 | 30 | .form-control, 31 | .input-group-addon { 32 | &:first-child { 33 | border-top-left-radius: 0; 34 | } 35 | } 36 | 37 | .input-group-btn:last-child > .btn { 38 | border-top-right-radius: 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/examples/examples.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular module for examples component. This component is divided to following logical components: 3 | * 4 | * frontend.examples.about 5 | * frontend.examples.author 6 | * frontend.examples.book 7 | * frontend.examples.chat 8 | * frontend.examples.messages 9 | * 10 | * Each component has it own configuration for ui-router. 11 | */ 12 | (function() { 13 | 'use strict'; 14 | 15 | // Define frontend.admin module 16 | angular.module('frontend.examples', [ 17 | 'frontend.examples.about', 18 | 'frontend.examples.author', 19 | 'frontend.examples.book', 20 | 'frontend.examples.chat', 21 | 'frontend.examples.messages' 22 | ]); 23 | 24 | // Module configuration 25 | angular.module('frontend.examples') 26 | .config([ 27 | '$stateProvider', 28 | function($stateProvider) { 29 | $stateProvider 30 | .state('examples', { 31 | parent: 'frontend', 32 | data: { 33 | access: 1 34 | }, 35 | views: { 36 | 'content@': { 37 | controller: [ 38 | '$state', 39 | function($state) { 40 | $state.go('examples.books'); 41 | } 42 | ] 43 | }, 44 | 'pageNavigation@': { 45 | templateUrl: '/frontend/core/layout/partials/navigation.html', 46 | controller: 'NavigationController', 47 | resolve: { 48 | _items: [ 49 | 'ContentNavigationItems', 50 | function resolve(ContentNavigationItems) { 51 | return ContentNavigationItems.getItems('examples'); 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | }) 58 | ; 59 | } 60 | ]) 61 | ; 62 | }()); 63 | -------------------------------------------------------------------------------- /src/app/examples/messages/messages-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all necessary Angular controller definitions for 'frontend.examples.messages' module. 3 | * 4 | * Note that this file should only contain controllers and nothing else. 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | /** 10 | * Message controller that demonstrates boilerplate error handling and usage of MessageService. 11 | * 12 | * @todo 13 | * 1) Make example about $http / $sailsSocket usage where automatic message is disabled. 14 | * 2) Make example about invalid JWT 15 | */ 16 | angular.module('frontend.examples.messages') 17 | .controller('MessagesController', [ 18 | '$scope', '$http', '$sailsSocket', 19 | 'MessageService', 'BackendConfig', 20 | function( 21 | $scope, $http, $sailsSocket, 22 | MessageService, BackendConfig 23 | ) { 24 | // Initialize used scope variables 25 | $scope.title = ''; 26 | $scope.message = ''; 27 | $scope.type = 'info'; 28 | $scope.messageTypes = [ 29 | 'info', 'success', 'warning', 'error' 30 | ]; 31 | 32 | // Specify invalid urls 33 | var urls = [ 34 | BackendConfig.url + '/Basdfasdf', 35 | BackendConfig.url + '/Book/123123123' 36 | ]; 37 | 38 | // Scope function to show specified message 39 | $scope.showMessage = function showMessage() { 40 | MessageService[$scope.type]($scope.message, $scope.title); 41 | }; 42 | 43 | // Function to make invalid HTTP request 44 | $scope.makeInvalidHttpRequest = function makeInvalidHttpRequest(type) { 45 | $http.get(urls[type]); 46 | }; 47 | 48 | // Function to make invalid socket request 49 | $scope.makeInvalidSailsSocketRequest = function makeInvalidSailsSocketRequest(type) { 50 | $sailsSocket.get(urls[type]); 51 | }; 52 | } 53 | ]) 54 | ; 55 | }()); 56 | -------------------------------------------------------------------------------- /src/app/examples/messages/messages-info.html: -------------------------------------------------------------------------------- 1 |

General info

2 | 3 |

4 | This is an example page to demonstrate how this boilerplate handles errors with $http and 5 | $sailsSocket requests. Note that this magic is done automatically via the error 6 | interceptor so you don't have to do anything extra for error handling. The error handling is done 7 | via the error interceptor that is attached to $http and $sailsSocket services. 8 |

9 | 10 |

11 | This interceptor will catch all errors from $http and $sailsSocket 12 | requests and show those to the user via the message service. Note that the message shown depends 13 | on the actual error response from the backend. 14 |

15 | 16 |

Functions in this example

17 | 18 |
19 |
20 | Custom messages 21 |
22 |
23 | Simple example to trigger different types of messages: info, success, warning and error with 24 | specified title and actual message. 25 |
26 | 27 |
28 | $http / $sailsSocket 29 |
30 |
31 | Examples to demonstrate invalid URL and not found record. These are handled automatically by ErrorInterceptor. 32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /src/app/examples/messages/messages.html: -------------------------------------------------------------------------------- 1 |

Messages

2 | 3 |

4 | This page demonstrates how you can use the Message service in your application. 5 | This service is automatically hooked to $http and $sailsSocket errors. 6 |

7 | 8 |
9 |
10 |
Message trigger from form data
11 |
12 |
13 | 14 |
15 | 18 |
19 |
20 |
21 | 22 |
23 | 26 |
27 |
28 |
29 | 30 |
31 | 35 |
36 |
37 |
38 |
39 | 43 |
44 |
45 |
46 |
47 |
48 |
Automatic message trigger via $http
49 | 50 |
51 | 54 | 55 | 58 |
59 | 60 |
Automatic message trigger via $sailsSocket
61 | 62 |
63 | 66 | 67 | 70 |
71 |
72 |
73 | 74 | -------------------------------------------------------------------------------- /src/app/examples/messages/messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Messages component which is divided to following logical components: 3 | * 4 | * Controllers 5 | * 6 | * All of these are wrapped to 'frontend.examples.messages' angular module. 7 | */ 8 | (function() { 9 | 'use strict'; 10 | 11 | // Define frontend.examples.messages angular module 12 | angular.module('frontend.examples.messages', []); 13 | 14 | // Module configuration 15 | angular.module('frontend.examples.messages') 16 | .config([ 17 | '$stateProvider', 18 | function config($stateProvider) { 19 | $stateProvider 20 | // Messages 21 | .state('examples.messages', { 22 | url: '/examples/messages', 23 | views: { 24 | 'content@': { 25 | templateUrl: '/frontend/examples/messages/messages.html', 26 | controller: 'MessagesController' 27 | } 28 | } 29 | }) 30 | ; 31 | } 32 | ]) 33 | ; 34 | }()); 35 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/app/styles/_angular.scss: -------------------------------------------------------------------------------- 1 | // scss-lint:disable SelectorFormat 2 | [ng\:cloak], 3 | [ng-cloak], 4 | [data-ng-cloak], 5 | [x-ng-cloak], 6 | .ng-cloak, 7 | .x-ng-cloak { 8 | // scss-lint:disable ImportantRule 9 | display: none !important; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/styles/_base.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | @import 'forms'; 3 | @import 'components'; 4 | @import 'bootstrap'; 5 | @import 'angular'; 6 | 7 | $color-footer-text: #999; 8 | $color-footer-hover-text: #3399f3; 9 | $color-footer-hover-icons: #337bd5; 10 | $color-tooltip-title: #fff; 11 | $color-tooltip-title-border: #9b9b9b; 12 | 13 | body { 14 | cursor: default; 15 | font-family: 'PT Sans', sans-serif; 16 | overflow-y: scroll; 17 | } 18 | 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h5 { 25 | &:first-of-type { 26 | margin-top: 10px; 27 | } 28 | } 29 | 30 | .container { 31 | max-width: 100%; 32 | min-width: 100%; 33 | } 34 | 35 | .main-container { 36 | margin-bottom: 30px; 37 | margin-top: 50px; 38 | } 39 | 40 | header .navbar-nav { 41 | float: none; 42 | } 43 | 44 | footer { 45 | .navbar { 46 | max-height: 25px; 47 | min-height: 25px; 48 | text-align: center; 49 | 50 | .navbar-nav { 51 | float: none; 52 | 53 | > li { 54 | display: inline-block; 55 | float: none; 56 | 57 | > a { 58 | font-size: 11px; 59 | line-height: 25px; 60 | padding: 0 10px; 61 | 62 | .fa { 63 | color: $color-footer-text; 64 | font-size: 14px; 65 | margin-right: 3px; 66 | position: relative; 67 | top: 1px; 68 | } 69 | 70 | &:hover { 71 | color: $color-footer-hover-text; 72 | 73 | .fa { 74 | color: $color-footer-hover-icons; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | .no-select { 84 | @include no-select(); 85 | } 86 | 87 | .pagination { 88 | margin: 0; 89 | } 90 | 91 | .text-medium { 92 | font-size: 90%; 93 | } 94 | 95 | .text-small { 96 | font-size: 80%; 97 | } 98 | 99 | .help { 100 | .fa { 101 | font-size: 20px; 102 | line-height: 14px; 103 | margin: 0 0 0 5px; 104 | position: relative; 105 | top: 2px; 106 | } 107 | } 108 | 109 | .tooltip { 110 | .title { 111 | border-bottom: 1px solid $color-tooltip-title-border; 112 | color: $color-tooltip-title; 113 | margin: 3px 0 5px; 114 | padding: 0; 115 | } 116 | 117 | .tooltip-inner { 118 | text-align: left; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/app/styles/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | $color-dropdown-menu-border-before: rgba(0, 0, 0, .2); 2 | $color-dropdown-menu-border-after: #fff; 3 | $color-navbar-link: #777; 4 | $color-navbar-link-hover: #3399f3; 5 | 6 | .dropdown-menu-arrow::before { 7 | border-bottom: 7px solid $color-dropdown-menu-border-before; 8 | border-left: 7px solid transparent; 9 | border-right: 7px solid transparent; 10 | content: ''; 11 | display: inline-block; 12 | left: 9px; 13 | position: absolute; 14 | top: -7px; 15 | } 16 | 17 | .dropdown-menu-arrow::after { 18 | border-bottom: 6px solid $color-dropdown-menu-border-after; 19 | border-left: 6px solid transparent; 20 | border-right: 6px solid transparent; 21 | content: ''; 22 | display: inline-block; 23 | left: 10px; 24 | position: absolute; 25 | top: -6px; 26 | } 27 | 28 | .pull-right.dropdown-menu-arrow::before, 29 | .btn-group.pull-right > .dropdown-menu-arrow::before { 30 | left: inherit; 31 | right: 9px; 32 | } 33 | 34 | .pull-right.dropdown-menu-arrow::after, 35 | .btn-group.pull-right > .dropdown-menu-arrow::after { 36 | left: inherit; 37 | right: 10px; 38 | } 39 | 40 | .navbar-default .navbar-nav > .active.not-active > a { 41 | color: $color-navbar-link; 42 | 43 | &:hover { 44 | color: $color-navbar-link-hover; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/styles/_components.scss: -------------------------------------------------------------------------------- 1 | .list-search { 2 | padding: 0; 3 | text-align: right; 4 | 5 | .fa.fa-plus-circle { 6 | position: relative; 7 | top: -6px; 8 | } 9 | 10 | .list-search-filters { 11 | max-width: 300px; 12 | 13 | form { 14 | margin: 0 0 -2px; 15 | padding: 0; 16 | position: relative; 17 | top: -9px; 18 | } 19 | 20 | .input-group-addon { 21 | cursor: pointer; 22 | padding: 0 8px; 23 | 24 | i { 25 | position: relative; 26 | top: 1px; 27 | } 28 | } 29 | 30 | ul { 31 | .title { 32 | font-size: 12px; 33 | padding: 3px 10px 0; 34 | text-transform: uppercase; 35 | white-space: nowrap; 36 | } 37 | } 38 | 39 | .dropdown-menu > li > a { 40 | padding-left: 10px; 41 | 42 | i { 43 | margin-right: 5px; 44 | } 45 | } 46 | } 47 | 48 | .list-search-filters, 49 | .pagination { 50 | display: inline-block; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/styles/_forms.scss: -------------------------------------------------------------------------------- 1 | $color-button-info: #fff; 2 | 3 | .editable-wrap { 4 | display: inline; 5 | } 6 | 7 | .editable-textarea { 8 | min-height: 350px; 9 | } 10 | 11 | .btn { 12 | &.active { 13 | .text-info { 14 | color: $color-button-info; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | $color-box-shadow: #ababab; 2 | 3 | @mixin border-radius($radius) { 4 | // If you want to support also old browsers just un-comment lines below 5 | //-webkit-border-radius: $radius; 6 | //-moz-border-radius: $radius; 7 | //-ms-border-radius: $radius; 8 | border-radius: $radius; 9 | } 10 | 11 | @mixin no-select() { 12 | // If you want to support also old browsers just un-comment lines below 13 | //-webkit-touch-callout: none; 14 | //-webkit-user-select: none; 15 | //-khtml-user-select: none; 16 | //-moz-user-select: none; 17 | //-ms-user-select: none; 18 | user-select: none; 19 | } 20 | 21 | @mixin box-shadow($top, $left, $blur, $color: $color-box-shadow, $inset: '') { 22 | // If you want to support also old browsers just un-comment lines below 23 | //-webkit-box-shadow: $top $left $blur $color #{$inset}; 24 | //-moz-box-shadow: $top $left $blur $color #{$inset}; 25 | box-shadow: $top $left $blur $color #{$inset}; 26 | } 27 | 28 | @mixin text-shadow($top, $left, $blur, $color) { 29 | text-shadow: $top $left $blur $color; 30 | } 31 | 32 | @mixin linear-gradient($color-from, $color-to) { 33 | // Fallback Color 34 | // If you want to support also old browsers just un-comment lines below 35 | //background-image: -webkit-gradient(linear, left top, left bottom, from($color-from), to($color-to)); // Saf4+, Chrome 36 | //background-image: -webkit-linear-gradient(top, $color-from, $color-to); // Chrome 10+, Saf5.1+, iOS 5+ 37 | //background-image: -moz-linear-gradient(top, $color-from, $color-to); // FF3.6 38 | //background-image: -ms-linear-gradient(top, $color-from, $color-to); // IE10 39 | //background-image: -o-linear-gradient(top, $color-from, $color-to); // Opera 11.10+ 40 | background: $color-to linear-gradient(top, $color-from, $color-to); 41 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#{$color-from}', EndColorStr='#{$color-to}'); 42 | } 43 | 44 | @mixin border-box() { 45 | // If you want to support also old browsers just un-comment lines below 46 | //-webkit-box-sizing: border-box; 47 | //-moz-box-sizing: border-box; 48 | box-sizing: border-box; 49 | } 50 | -------------------------------------------------------------------------------- /src/app/styles/modal.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | 3 | $color-border: #ccc; 4 | $color-header-start: #fff; 5 | $color-header-end: #eee; 6 | $color-footer-start: #eee; 7 | $color-footer-end: #fff; 8 | 9 | .modal { 10 | overflow: auto; 11 | 12 | .modal-title { 13 | margin: 0; 14 | } 15 | } 16 | 17 | .modal-open { 18 | overflow-y: scroll; 19 | } 20 | 21 | .modal-header, 22 | .modal-footer { 23 | @include no-select(); 24 | border-color: $color-border; 25 | } 26 | 27 | .modal-header { 28 | @include linear-gradient($color-header-start, $color-header-end); 29 | border-top-left-radius: 6px; 30 | border-top-right-radius: 6px; 31 | padding: 10px 15px; 32 | 33 | .close { 34 | font-size: 45px; 35 | margin-top: -10px; 36 | } 37 | } 38 | 39 | .modal-body { 40 | @include no-select(); 41 | 42 | overflow-x: auto; 43 | overflow-y: scroll; 44 | 45 | &.modal-help { 46 | padding: 0 15px; 47 | } 48 | } 49 | 50 | .modal-footer { 51 | @include linear-gradient($color-footer-start, $color-footer-end); 52 | border-bottom-left-radius: 6px; 53 | border-bottom-right-radius: 6px; 54 | margin-top: 0; 55 | padding: 10px; 56 | } 57 | -------------------------------------------------------------------------------- /src/dummy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Dummy JS file for smart IDEs like php/webStorm. 5 | * 6 | * Purpose of this file is to help IDE to use autocomplete features. 7 | */ 8 | 9 | var layout = { 10 | menuItem: { 11 | state: string, 12 | title: string, 13 | access: number 14 | } 15 | }; 16 | 17 | var settings = { 18 | backendUrl: string, 19 | frontend: { 20 | ports: { 21 | production: number, 22 | development: number 23 | } 24 | } 25 | }; 26 | --------------------------------------------------------------------------------