├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .tern-project ├── BANNER.txt ├── CHANGELOG.md ├── LICENSE ├── README.md ├── data ├── sample-products.json ├── sample-products.xlsx ├── sample-transactions.json └── sample-transactions.xlsx ├── domo ├── manifest.json └── thumbnail.png ├── jsconfig.json ├── karma.conf.js ├── lab.html ├── other ├── boilerplate-utils.js ├── init-repo.es6.js ├── karma.conf.es6.js ├── server.es6.js ├── setup.js ├── update.js └── webpack.config.es6.js ├── package.json ├── plopfile.js ├── server.js ├── src ├── common │ ├── components │ │ └── README.md │ ├── filters │ │ ├── README.md │ │ ├── default │ │ │ ├── default.filter.js │ │ │ └── default.filter.spec.js │ │ └── summary │ │ │ ├── summary.filter.js │ │ │ └── summary.filter.spec.js │ ├── index.js │ ├── services │ │ ├── da-events │ │ │ ├── da-events.factory.js │ │ │ └── da-events.factory.spec.js │ │ ├── date-range-items │ │ │ ├── date-range-items.value.js │ │ │ └── date-range-items.value.spec.js │ │ ├── global-filters-factory │ │ │ ├── global-filters-factory.factory.js │ │ │ └── global-filters-factory.factory.spec.js │ │ ├── granularity-items │ │ │ ├── granularity-items.value.js │ │ │ └── granularity-items.value.spec.js │ │ ├── product-table-header │ │ │ ├── product-table-header.value.js │ │ │ └── product-table-header.value.spec.js │ │ ├── products-factory │ │ │ ├── dev-products-factory.factory.js │ │ │ ├── prod-products-factory.factory.js │ │ │ ├── products-factory.factory.js │ │ │ └── products-factory.factory.spec.js │ │ ├── transaction-pill-data-factory │ │ │ ├── transaction-pill-data.factory.js │ │ │ └── transaction-pill-data.factory.spec.js │ │ └── transactions-analytics-factory │ │ │ ├── dev-transactions-analytics-factory.factory.js │ │ │ ├── prod-transactions-analytics-factory.factory.js │ │ │ ├── transactions-analytics-factory.factory.js │ │ │ └── transactions-analytics-factory.factory.spec.js │ └── styles │ │ ├── typebase.css │ │ └── variables.css ├── desktop │ ├── components │ │ ├── README.md │ │ ├── loading-mask │ │ │ ├── loading-mask.component.css │ │ │ ├── loading-mask.component.html │ │ │ ├── loading-mask.component.js │ │ │ └── loading-mask.component.spec.js │ │ └── toolbar │ │ │ ├── toolbar.component.css │ │ │ ├── toolbar.component.html │ │ │ ├── toolbar.component.js │ │ │ └── toolbar.component.spec.js │ ├── containers │ │ ├── README.md │ │ └── tabs-container │ │ │ ├── tabs-container.component.css │ │ │ ├── tabs-container.component.html │ │ │ ├── tabs-container.component.js │ │ │ └── tabs-container.component.spec.js │ ├── desktop.config.js │ ├── desktop.css │ ├── desktop.html │ ├── desktop.init.js │ ├── index.js │ └── routes │ │ ├── README.md │ │ ├── products │ │ ├── components │ │ │ ├── README.md │ │ │ ├── inventory-tools │ │ │ │ ├── inventory-tools.component.css │ │ │ │ ├── inventory-tools.component.html │ │ │ │ ├── inventory-tools.component.js │ │ │ │ └── inventory-tools.component.spec.js │ │ │ ├── product-table │ │ │ │ ├── product-table.component.css │ │ │ │ ├── product-table.component.html │ │ │ │ ├── product-table.component.js │ │ │ │ └── product-table.component.spec.js │ │ │ └── search-bar │ │ │ │ ├── search-bar.component.css │ │ │ │ ├── search-bar.component.html │ │ │ │ ├── search-bar.component.js │ │ │ │ └── search-bar.component.spec.js │ │ ├── containers │ │ │ └── inventory-container │ │ │ │ ├── inventory-container.component.css │ │ │ │ ├── inventory-container.component.html │ │ │ │ ├── inventory-container.component.js │ │ │ │ └── inventory-container.component.spec.js │ │ └── index.js │ │ └── transactions │ │ ├── components │ │ ├── README.md │ │ ├── line-chart │ │ │ ├── line-chart.component.js │ │ │ └── line-chart.component.spec.js │ │ ├── pill │ │ │ ├── pill.component.js │ │ │ └── pill.component.spec.js │ │ └── transaction-tools │ │ │ ├── transaction-tools.component.css │ │ │ ├── transaction-tools.component.html │ │ │ ├── transaction-tools.component.js │ │ │ └── transaction-tools.component.spec.js │ │ ├── containers │ │ ├── pills-container │ │ │ ├── pills-container.component.css │ │ │ ├── pills-container.component.html │ │ │ ├── pills-container.component.js │ │ │ └── pills-container.component.spec.js │ │ └── transactions-container │ │ │ ├── transactions-container.component.css │ │ │ ├── transactions-container.component.html │ │ │ ├── transactions-container.component.js │ │ │ └── transactions-container.component.spec.js │ │ └── index.js ├── responsive │ ├── components │ │ └── README.md │ ├── containers │ │ └── README.md │ ├── index.js │ ├── responsive.config.js │ ├── responsive.css │ ├── responsive.html │ ├── responsive.init.js │ └── routes │ │ └── README.md └── switcher.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-loose", "stage-1"], 3 | "plugins": ["transform-runtime", "add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "env": { 4 | "mocha": true, 5 | "browser": true, 6 | "node": true, 7 | "es6": true 8 | }, 9 | "rules": { 10 | "comma-dangle": [0, "always-multiline"], 11 | "no-use-before-define": [2, "nofunc"], 12 | "spaced-comment": [0, "never"] 13 | }, 14 | "globals": { 15 | "ON_DEV": false, 16 | "ON_TEST": false, 17 | "ON_PROD": false, 18 | "sinon": false, 19 | "expect": false, 20 | "angular": false, 21 | "inject": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Domo Apps specific 2 | dist 3 | 4 | ############################################################################ 5 | # NodeJS specific gitignore 6 | # https://github.com/github/gitignore/blob/master/Node.gitignore 7 | 8 | 9 | # Logs 10 | logs 11 | *.log 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # Commenting this out is preferred by some people, see 32 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 33 | node_modules 34 | 35 | # Users Environment Variables 36 | .lock-wscript 37 | 38 | 39 | 40 | ############################################################################ 41 | # OSX gitignore 42 | # https://github.com/github/gitignore/blob/master/Global/OSX.gitignore 43 | 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | 48 | # Icon must end with two \r 49 | Icon 50 | 51 | 52 | # Thumbnails 53 | ._* 54 | 55 | # Files that might appear on external disk 56 | .Spotlight-V100 57 | .Trashes 58 | 59 | # Directories potentially created on remote AFP share 60 | .AppleDB 61 | .AppleDesktop 62 | Network Trash Folder 63 | Temporary Items 64 | .apdisk 65 | 66 | 67 | 68 | ############################################################################ 69 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 70 | # https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 71 | 72 | /*.iml 73 | 74 | ## Directory-based project format: 75 | .idea/ 76 | # if you remove the above rule, at least ignore the follwing: 77 | 78 | # User-specific stuff: 79 | # .idea/workspace.xml 80 | # .idea/tasks.xml 81 | # .idea/dictionaries 82 | 83 | # Sensitive or high-churn files: 84 | # .idea/dataSources.ids 85 | # .idea/dataSources.xml 86 | # .idea/sqlDataSources.xml 87 | # .idea/dynamic.xml 88 | # .idea/uiDesigner.xml 89 | 90 | # Gradle: 91 | # .idea/gradle.xml 92 | # .idea/libraries 93 | 94 | # Mongo Explorer plugin: 95 | # .idea/mongoSettings.xml 96 | 97 | ## File-based project format: 98 | *.ipr 99 | *.iws 100 | 101 | ## Plugin-specific files: 102 | 103 | # IntelliJ 104 | out/ 105 | 106 | # mpeltonen/sbt-idea plugin 107 | .idea_modules/ 108 | 109 | # JIRA plugin 110 | atlassian-ide-plugin.xml 111 | 112 | # Crashlytics plugin (for Android Studio and IntelliJ) 113 | com_crashlytics_export_strings.xml 114 | 115 | 116 | 117 | ############################################################################ 118 | # SublimeText gitignore 119 | # https://help.github.com/articles/ignoring-files 120 | 121 | # cache files for sublime text 122 | *.tmlanguage.cache 123 | *.tmPreferences.cache 124 | *.stTheme.cache 125 | 126 | # workspace files are user-specific 127 | *.sublime-workspace 128 | 129 | # project files should be checked into the repository, unless a significant 130 | # proportion of contributors will probably not be using SublimeText 131 | # *.sublime-project 132 | 133 | # sftp configuration file 134 | sftp-config.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | docs/ 3 | tests/ 4 | **/.* -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 6, 3 | "libs": [ 4 | "browser", 5 | "jquery" 6 | ], 7 | "loadEagerly": [ 8 | "src/**/*.js" 9 | ], 10 | "dontLoad": [ 11 | "dist/**/*.js" 12 | ], 13 | "plugins": { 14 | "complete_strings": {}, 15 | "angular": {}, 16 | "modules": {}, 17 | "es_modules": {}, 18 | "doc_comment": { 19 | "fullDocs": true 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BANNER.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 Domo, Inc - All Rights Reserved 2 | Unauthorized copying of any files, via any medium is strictly prohibited 3 | Proprietary and confidential -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.1.0](https://github.com/DomoApps/sample-app/compare/v0.1.0...v0.1.1) (2016-08-18) 3 | 4 | ### Bugfixes 5 | * **Transactions Container**: fix a merge issue that broke the date range and date grain selects 6 | 7 | 8 | ## [0.1.0](https://github.com/DomoApps/sample-app/compare/v0.0.1...v0.1.0) (2016-08-03) 9 | 10 | ### Features 11 | * **DataSources**: add XLSX files for easier datasource creation in Domo 12 | 13 | ### Misc 14 | * **Sample Data**: move sample data to `/data` folder 15 | * **Readme**: clean up readme and clarify datasource creation 16 | 17 | 18 | 19 | ## [0.0.1](https://github.com/DomoApps/sample-app/compare/v0.0.1...v0.0.1) (2016-07-27) 20 | 21 | Initial release, enjoy! 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Advanced Sample App 3 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 4 | 5 | ![App Thumbnail](domo/thumbnail.png) 6 | 7 | ## What is the Advanced Sample App? 8 | This app was created to demonstrate how to use Domo's App [Starter Kit](https://github.com/DomoApps/starter-kit) and how commonly requested functionality is implemented in a robust custom app design. 9 | 10 | The app demonstrates data formatting, filtering, and display. An "inventory" of products is displayed as well as summaries of transactions. The "Transactions" page demonstrates the use of widgets. Domo Widgets are reusable components that render [d3 charts](https://d3js.org). 11 | 12 | Because this app design is not attached to an instance of Domo, dataset requests are mocked in the products and transaction analytics factories. Comments are included to explain how production dataset requests are made. See the [Domo Developer Guide](https://developer.domo.com/docs/dev-studio/dev-studio-data) for more information on dataset requests. 13 | 14 | ## How Do I Use the Advanced Sample App? 15 | 16 | ### 1. Set Up Your Project 17 | 1. Clone this repo 18 | `$ git clone https://github.com/DomoApps/advanced-sample-app.git` 19 | 2. Install dependencies `$ npm install` 20 | 21 | ### 2. Development 22 | `$ npm start` will run a development server that reloads on file changes 23 | 24 | ### 3. Publishing to Domo 25 | 1. Login to Domo `$ domo login` 26 | 2. Publish the app design `$ npm run upload` 27 | 3. Update the `{ id: ... }` value in `domo/manifest.json` with your new app design ID 28 | 29 | ### Datasets 30 | The app is configured by default to mock any dataset requests with JSON files. In order to change this functionality to use "live" data: 31 | 32 | 1. Follow instructions on [Domo University](https://knowledge.domo.com/?cid=connectordataset#Adding_a_DataSet_using_a_connector) to create two Excel datasets. Both Excel files are located in the `/data` folder 33 | 2. Navigate to the two datasets in Domo and copy the `ID`s from the URL (https://{COMPANY}.domo.com/excel/{ID}/overview) 34 | 3. Replace the existing `ID`s in `manifest.json` with the new ones from step 2 35 | 4. In `src/desktop/index.js` change the value of `MOCK_REQUESTS` to `false` 36 | 5. Run `npm run upload` 37 | 6. Create a new card using the new custom app design by following the steps on the [Developer Portal](https://developer.domo.com/docs/dev-studio/dev-studio-publish#Create%20an%20App%20Instance) 38 | 39 | For more information on available commands and usage, see the documentation for Domo's App [Starter Kit](https://github.com/DomoApps/starter-kit). 40 | 41 | ## Compatiblity 42 | #### Tested and working 43 | - Chrome (OSX) 44 | - Safari (OSX) 45 | 46 | #### Known Issues 47 | - Windows 48 | - Sticky table header does not align with table body 49 | 50 | ## Folder Structure 51 | ```text 52 | . // top level config stuff for webpack, karma, eslint, ect... 53 | ├── src 54 | | ├── common // common across desktop and responsive 55 | | | ├── components // place for common components 56 | | | | 57 | | | ├── filters // place for common filters 58 | | | | 59 | | | ├── services // place for common services 60 | | | | 61 | | | ├── styles // place for common styles 62 | | | | ├── typebase.css // base type for all apps 63 | | | | └── variable.css // variables 64 | | | └── index.js // JS entry for common Angular module 65 | | | 66 | | ├── responsive // a folder for each component 67 | | | ├── components // place for dumb/presenter components common across routes 68 | | | | 69 | | | ├── containers // place for smart/container components common across routes 70 | | | | 71 | | | ├── routes // place for routes 72 | | | | └── my-route 73 | | | | ├── components // place for dumb/presenter components specific to this route 74 | | | | | 75 | | | | ├── containers // place for smart/container components specific to this route 76 | | | | | └── my-container 77 | | | | | ├── my-container.component.js 78 | | | | | ├── my-container.component.css 79 | | | | | └── my-container.component.html 80 | | | | | 81 | | | | └── index.js // define module and route 82 | | | | 83 | | | ├── responsive.config.js // responsive app top level configuration 84 | | | ├── responsive.init.js // top level initialization code 85 | | | ├── responsive.html // html entry (layout html goes here) 86 | | | ├── responsive.css // common css for responsive 87 | | | └── index.js // JS entry 88 | | | 89 | | └── desktop // same structure as responsive 90 | | 91 | └── dist // Generated by build 92 | ... 93 | 94 | ``` 95 | -------------------------------------------------------------------------------- /data/sample-products.json: -------------------------------------------------------------------------------- 1 | [{"category":"Food","name":"Blue Milk","price":1.99,"quantity":2},{"category":"Apparel","name":"Padawan Braid","price":99.99,"quantity":13},{"category":"Lawn Care","name":"Bantha Fodder","price":0.49,"quantity":1},{"category":"Weapons","name":"Thermal Detonator","price":15.79,"quantity":13},{"category":"Wall Decor","name":"Carbonite Casing","price":1445.69,"quantity":67},{"category":"Apparel","name":"Mandalorian Armor","price":100000000.49,"quantity":2},{"category":"Appliances","name":"R2 Unit","price":687.49,"quantity":0},{"category":"Weapons","name":"Ventral Cannons","price":11945.49,"quantity":0},{"category":"Pets","name":"Rancor","price":4339.0,"quantity":0},{"category":"Food","name":"Nerf (5 Pack)","price":4.99,"quantity":17},{"category":"Sporting Goods","name":"Dejarik Set","price":34.99,"quantity":6},{"category":"Sporting Goods","name":"Seeker Droid","price":99.99,"quantity":1},{"category":"Appliances","name":"Moisture vaporator","price":114.99,"quantity":13},{"category":"Apparel","name":"Utility Belt","price":45.99,"quantity":13},{"category":"Pets","name":"Sarlacc","price":3.0E+7,"quantity":8},{"category":"Weapons","name":"DL-44","price":9999.0,"quantity":16},{"category":"Food","name":"Portion bread","price":4.5,"quantity":18},{"category":"Weapons","name":"Double-bladed lightsaber","price":5.0E+7,"quantity":4},{"category":"Sporting Goods","name":"Power couplings","price":149.99,"quantity":13}] 2 | -------------------------------------------------------------------------------- /data/sample-products.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomoApps/advanced-sample-app/a2980ae942ff19e0d14fc0026b3095ec5bc2bc4c/data/sample-products.xlsx -------------------------------------------------------------------------------- /data/sample-transactions.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomoApps/advanced-sample-app/a2980ae942ff19e0d14fc0026b3095ec5bc2bc4c/data/sample-transactions.xlsx -------------------------------------------------------------------------------- /domo/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SampleApp", 3 | "mapping": [ 4 | { 5 | "alias": "products", 6 | "dataSetId": "8b105d2d-350b-4a52-b539-6a1eb25bfa32", 7 | "fields": [ 8 | { 9 | "alias": "category", 10 | "columnName": "category" 11 | }, 12 | { 13 | "alias": "name", 14 | "columnName": "name" 15 | }, 16 | { 17 | "alias": "price", 18 | "columnName": "price" 19 | }, 20 | { 21 | "alias": "quantity", 22 | "columnName": "quantity" 23 | } 24 | ] 25 | }, 26 | { 27 | "alias": "transactions", 28 | "dataSetId": "e46eca50-0547-4684-81b0-eeca3317e106", 29 | "fields": [ 30 | { 31 | "alias": "date", 32 | "columnName": "date" 33 | }, 34 | { 35 | "alias": "category", 36 | "columnName": "category" 37 | }, 38 | { 39 | "alias": "name", 40 | "columnName": "name" 41 | }, 42 | { 43 | "alias": "price", 44 | "columnName": "price" 45 | }, 46 | { 47 | "alias": "quantity", 48 | "columnName": "quantity" 49 | }, 50 | { 51 | "alias": "total", 52 | "columnName": "total" 53 | } 54 | ] 55 | } 56 | ], 57 | "version": "0.0.1", 58 | "size": { 59 | "width": 5, 60 | "height": 3 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /domo/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomoApps/advanced-sample-app/a2980ae942ff19e0d14fc0026b3095ec5bc2bc4c/domo/thumbnail.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs" 5 | }, 6 | "files": [ 7 | "src/common/index.js", 8 | "src/desktop/index.js", 9 | "src/responsive/index.js" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | module.exports = require('./other/karma.conf.es6'); 3 | -------------------------------------------------------------------------------- /lab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 141 | 142 | 143 | 144 | 145 |
146 |
147 | 148 |
1165 x 862
149 |
150 | 151 |
152 |
153 | 154 |
375 x 667
155 |
156 |
157 | 158 | 169 | 213 | {% if (o.htmlWebpackPlugin.options.dev) { %}{% } %} 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /other/boilerplate-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use to attach multiple Angular components, services, etc. to an Angular module. 3 | * Takes a required context from webpack's required.context 4 | * and maps it to an array of modules to be attached to an 5 | * Angular module. 6 | * 7 | * @param {webpackContext} context Webpack context object 8 | * @return {function} Used to attach an array of modules to an Angular module 9 | */ 10 | export function attachAll(context) { 11 | const modules = context.keys().map(context); 12 | return function attachModules(ngModule) { 13 | modules.forEach(module => module(ngModule)); 14 | }; 15 | } 16 | 17 | 18 | /** 19 | * Use to get names for multiple Angular modules to inject as dependencies on an Angular module. 20 | * Context must be Angular modules i.e. require.context('./dir', /file\.js/) must 21 | * reference files that export an Angular module. 22 | * 23 | * @param {webpackContext} context Webpack context object from require.context(). 24 | * @return {Array} An array of Angular module names 25 | */ 26 | export function getNgModuleNames(context) { 27 | const modules = context.keys().map(context); 28 | return modules.map(module => module.name); 29 | } 30 | -------------------------------------------------------------------------------- /other/init-repo.es6.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import inquirer from 'inquirer'; 5 | import kebabCase from 'lodash.kebabcase'; 6 | import shelljs from 'shelljs'; 7 | 8 | // TODO: update with the repo location on Github after we migrate 9 | const STARTER_REPO = 'git@git.empdev.domo.com:AppTeam6/da-webpack.git'; 10 | const GENERATOR_KEYWORDS = ['da-webpack', 'starter-kit']; 11 | 12 | const DOMO_STRICT_LICENSE = [ 13 | 'Copyright (C) 2016 Domo, Inc - All Rights Reserved', 14 | 'Unauthorized copying of any files, via any medium is strictly prohibited', 15 | 'Proprietary and confidential' 16 | ].join('\n'); 17 | 18 | const QUESTIONS = [ 19 | { 20 | type: 'input', 21 | name: 'name', 22 | message: `what is your app's name?`, 23 | }, 24 | { 25 | type: 'input', 26 | name: 'description', 27 | message: 'what is a brief description of your app?', 28 | }, 29 | { 30 | type: 'input', 31 | name: 'git', 32 | message: 'what is your git url?' 33 | } 34 | ]; 35 | 36 | const remotes = getRemotesAsMap(); 37 | if (remotes.has('generator')) { 38 | console.log(`Your repo is already setup to receive dev tool updates. Run ${chalk.bold('npm run update-tools')} for updates.`); 39 | } else if (hasChangedOriginRemote(remotes)) { 40 | addGeneratorRemote(); 41 | } else { 42 | initializeProject(); 43 | } 44 | 45 | function getRemotesAsMap() { 46 | const remotes = new Map(); 47 | 48 | const remotesOutput = shelljs.exec('git remote -v', { silent: true }).output; 49 | remotesOutput.trim() 50 | .split('\n') 51 | .filter(line => line.indexOf('(fetch)') !== -1) 52 | .forEach(remoteLine => { 53 | const [name, verboseLocation] = remoteLine.split('\t'); 54 | const location = verboseLocation.replace(' (fetch)', ''); 55 | remotes.set(name, location); 56 | }); 57 | 58 | return remotes; 59 | } 60 | 61 | function hasChangedOriginRemote(remotes) { 62 | const originRemote = remotes.get('origin'); 63 | 64 | let hasChanged = true; 65 | for (const keyword of GENERATOR_KEYWORDS) { 66 | if (originRemote.indexOf(keyword) !== -1) { 67 | hasChanged = false; 68 | break; 69 | } 70 | } 71 | 72 | return hasChanged; 73 | } 74 | 75 | function addGeneratorRemote() { 76 | const results = shelljs.exec(`git remote add generator ${STARTER_REPO}`); 77 | if (results.code === 0) { 78 | console.log(`${chalk.green('SUCCESS!')} The ${chalk.bold('generator')} remote has been setup. ` + 79 | `Run ${chalk.bold('npm run update-tools')} for updates.`); 80 | } else { 81 | console.log(`${chalk.red('ERROR:')} ${results.output}`); 82 | } 83 | } 84 | 85 | function initializeProject() { 86 | inquirer.prompt(QUESTIONS, answers => { 87 | setupPackage({ name: kebabCase(answers.name), description: answers.description, git: answers.git }) 88 | .then(setupGit) 89 | .then(replaceLicense) 90 | .then(() => { 91 | console.log(chalk.green('Success!')); 92 | console.log(chalk.white('Don\'t forget to setup your manifest.json and run `npm run upload`')); 93 | }) 94 | .catch((err) => { 95 | console.log(chalk.red('There was an error!')); 96 | console.error(err); 97 | }); 98 | }); 99 | } 100 | 101 | function setupPackage({ name, description, git }) { 102 | return new Promise((resolve, reject) => { 103 | const filePath = path.resolve(__dirname, '../package.json'); 104 | fs.readFile(filePath, (err, data) => { 105 | if (err) reject(err); 106 | 107 | const p = JSON.parse(data); 108 | p.name = name; 109 | p.version = '0.0.1'; 110 | p.description = description; 111 | p.repository = git; 112 | 113 | fs.writeFile(filePath, JSON.stringify(p, null, 2), (err, data) => { 114 | if (err) reject(err); 115 | 116 | resolve({ name, description, git }); 117 | }); 118 | }); 119 | }); 120 | } 121 | 122 | function setupGit({ name, description, git }) { 123 | shelljs.exec('git remote rename origin generator'); 124 | shelljs.exec(`git remote add origin ${git}`); 125 | shelljs.exec('git push -u origin master'); 126 | } 127 | 128 | function replaceLicense() { 129 | return new Promise((resolve, reject) => { 130 | const licensePath = path.resolve(__dirname, '../LICENSE'); 131 | fs.writeFile(licensePath, DOMO_STRICT_LICENSE, (err, data) => { 132 | if (err) return reject(err); 133 | 134 | resolve(); 135 | }); 136 | }); 137 | } 138 | 139 | 140 | -------------------------------------------------------------------------------- /other/karma.conf.es6.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // load webpack config here for for webpack preprocessor 4 | const webpackConfig = require('../webpack.config'); 5 | delete webpackConfig.devtool; 6 | webpackConfig.cache = true; 7 | 8 | let file; 9 | // Add new cdns resouces here. 10 | const cdns = [ 11 | 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.10/d3.min.js', 12 | 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js', 13 | 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js', 14 | 'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.3/angular.min.js', 15 | 'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.3/angular-animate.min.js', 16 | 'https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.min.js', 17 | ]; 18 | 19 | const entry = [ 20 | ...cdns, 21 | 'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.3/angular-mocks.js', 22 | ]; 23 | const preprocessors = {}; 24 | for (const chunk in webpackConfig.entry) { 25 | if ({}.hasOwnProperty.call(webpackConfig.entry, chunk)) { 26 | file = path.resolve(webpackConfig.context, webpackConfig.entry[chunk]); 27 | entry.push(file); 28 | preprocessors[file] = ['webpack']; 29 | } 30 | } 31 | 32 | module.exports = (config) => { 33 | config.set({ 34 | basePath: './', 35 | frameworks: ['mocha', 'chai', 'sinon'], 36 | files: entry, 37 | webpack: webpackConfig, 38 | 39 | webpackMiddleware: { 40 | noInfo: true 41 | }, 42 | 43 | // list of files to exclude 44 | exclude: [ 45 | 'src/switcher.js' 46 | ], 47 | 48 | // preprocess matching files before serving them to the browser 49 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 50 | preprocessors: preprocessors, 51 | 52 | reporters: ['dots'], 53 | port: 9876, 54 | colors: true, 55 | autoWatch: true, 56 | browsers: ['PhantomJS', 'Chrome', 'Firefox', 'Safari'], 57 | plugins: [ 58 | require('karma-webpack'), 59 | 'karma-coverage', 60 | 'karma-phantomjs-launcher', 61 | 'karma-chrome-launcher', 62 | 'karma-firefox-launcher', 63 | 'karma-safari-launcher', 64 | 'karma-mocha', 65 | 'karma-chai', 66 | 'karma-sinon', 67 | ], 68 | logLevel: config.LOG_ERROR 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /other/server.es6.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | // webpack 3 | const WebpackDevServer = require('webpack-dev-server'); 4 | const webpack = require('webpack'); 5 | const webpackConfig = require('../webpack.config'); 6 | webpackConfig.output.path = '/'; 7 | 8 | const compiler = webpack(webpackConfig); 9 | 10 | // proxy 11 | const path = require('path'); 12 | const fs = require('fs-extra'); 13 | const glob = require('glob'); 14 | const request = require('request'); 15 | const bodyParser = require('body-parser'); 16 | const Domo = require('ryuu-client'); 17 | const portfinder = require('portfinder'); 18 | portfinder.basePort = 3000; 19 | 20 | let baseUrl; 21 | const home = Domo.getHomeDir(); 22 | const mostRecent = getMostRecentLogin(); 23 | const manifest = fs.readJsonSync(path.resolve(process.cwd() + '/domo/manifest.json')); 24 | const domainPromise = getDomoappsDomain() 25 | .then(_baseUrl => baseUrl = _baseUrl) 26 | .then(() => createContext(manifest.id, manifest.mapping)); 27 | 28 | // webpack-dev-server 29 | const server = new WebpackDevServer(compiler, { 30 | contentBase: 'dist/', 31 | hot: false, 32 | noInfo: true, // set to false if you want to see build info 33 | stats: { 34 | colors: true 35 | } 36 | }); 37 | 38 | server.app.use(bodyParser.urlencoded({ 39 | extended: false 40 | })); 41 | server.app.use(bodyParser.json()); 42 | 43 | // domo data service proxy 44 | server.app.all('/data/v1/:query', proxyRequest); 45 | 46 | function proxyRequest(req, res) { 47 | domainPromise 48 | .then(context => { 49 | const j = request.jar(); 50 | const url = baseUrl + req.url; 51 | const auth = `DA-SID-${getCustomer()}="${mostRecent.sid}"`; 52 | const cookie = request.cookie(auth); 53 | j.setCookie(cookie, baseUrl); 54 | 55 | const referer = req.headers.referer.indexOf('?') >= 0 ? `${req.headers.referer}&context=${context.id}` : `${req.headers.referer}?userId=27&customer=dev&locale=en-US&platform=desktop&context=${context.id}`; // jshint ignore:line 56 | 57 | const r = request({ 58 | url, 59 | method: req.method, 60 | jar: j, 61 | headers: { 62 | 'content-type': req.headers['content-type'] || req.headers['Content-Type'], 63 | referer, 64 | accept: req.headers.accept 65 | }, 66 | body: JSON.stringify(req.body) 67 | }); 68 | 69 | r.pipe(res); 70 | }) 71 | .catch(err => { 72 | console.warn(err); 73 | }); 74 | } 75 | 76 | // start server 77 | checkSession() 78 | .then(() => { 79 | portfinder.getPort({ 80 | host: '0.0.0.0' 81 | }, (err, port) => { 82 | server.listen(port, '0.0.0.0', () => { 83 | console.log(`Listening on http://0.0.0.0:${port}/webpack-dev-server/index.html`); 84 | }); 85 | }); 86 | }) 87 | .catch(() => { 88 | console.warn('Session expired. Please login again using domo login.'); 89 | }); 90 | 91 | // helpers 92 | function getMostRecentLogin() { 93 | const logins = glob.sync(`${home}/login/*.json`); 94 | if (logins.length === 0) { 95 | return null; 96 | } 97 | 98 | const mostRecentLogin = logins.reduce((prev, next) => { 99 | return fs.statSync(prev).mtime > fs.statSync(next).mtime ? prev : next; 100 | }); 101 | return fs.readJsonSync(mostRecentLogin); 102 | } 103 | 104 | function getCustomer() { 105 | const regexp = /([\w]+)[\.|-]/; 106 | return mostRecent.instance.match(regexp)[1]; 107 | } 108 | 109 | function getEnv() { 110 | const regexp = /([-_\w]+)\.(.*)/; 111 | return mostRecent.instance.match(regexp)[2]; 112 | } 113 | 114 | function getDomoappsDomain() { 115 | const uuid = Domo.createUUID(); 116 | const j = request.jar(); 117 | const auth = `SID="${mostRecent.sid}"`; 118 | const cookie = request.cookie(auth); 119 | j.setCookie(cookie, `https://${mostRecent.instance}`); 120 | return new Promise((resolve) => { 121 | request({ 122 | url: `https://${mostRecent.instance}/api/content/v1/mobile/environment`, 123 | jar: j 124 | }, (err, res) => { 125 | if (res.statusCode === 200) { 126 | resolve(`https://${uuid}.${JSON.parse(res.body).domoappsDomain}`); 127 | } else { 128 | resolve(`https://${uuid}.domoapps.${getEnv()}`); 129 | } 130 | }); 131 | }); 132 | } 133 | 134 | function createContext(designId, mapping) { 135 | return new Promise(resolve => { 136 | const options = { 137 | url: `https://${mostRecent.instance}/domoapps/apps/v2/contexts`, 138 | method: 'POST', 139 | json: { 140 | designId, 141 | mapping 142 | }, 143 | headers: { 144 | 'X-Domo-Authentication': mostRecent.sid 145 | } 146 | }; 147 | 148 | request(options, (err, res) => { 149 | resolve(res.body[0] ? res.body[0] : { 150 | id: 0 151 | }); 152 | }); 153 | }); 154 | } 155 | 156 | function checkSession() { 157 | return new Promise((resolve, reject) => { 158 | const options = { 159 | url: `https://${mostRecent.instance}/auth/validate`, 160 | method: 'GET', 161 | headers: { 162 | 'X-Domo-Authentication': mostRecent.sid 163 | } 164 | }; 165 | 166 | request(options, (err, res) => { 167 | try { 168 | const isValid = JSON.parse(res.body) 169 | .isValid; 170 | if (isValid) { 171 | resolve(true); 172 | } else { 173 | reject(false); 174 | } 175 | } catch (e) { 176 | // couldn't parse as JSON which means the service doesn't exist yet. 177 | // TODO: remove this once the /domoweb/auth/validate service has shipped to prod 178 | resolve(true); 179 | } 180 | }); 181 | }); 182 | } 183 | -------------------------------------------------------------------------------- /other/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | module.exports = require('./init-repo.es6'); 3 | -------------------------------------------------------------------------------- /other/update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const shelljs = require('shelljs'); 4 | const path = require('path'); 5 | const chalk = require('chalk'); 6 | 7 | update(); 8 | 9 | 10 | ////////////////////////////////////////////// 11 | 12 | 13 | function update() { 14 | if (hasUncommittedChanges()) { 15 | console.log(`${chalk.red('ERROR:')} you have uncommitted changes. Please commit or stash them before updating.`); 16 | process.exit(1); 17 | } 18 | 19 | if (!hasGeneratorRemote()) { 20 | console.log(`${chalk.red('ERROR:')} no ${chalk.bold('generator')} remote found. Exiting.`); 21 | process.exit(1); 22 | } 23 | 24 | shelljs.exec('git fetch generator'); 25 | shelljs.exec('git merge --no-commit generator/master'); 26 | 27 | console.log(`${chalk.yellow('WARNING:')} Make sure to review the changes with ${chalk.bold('git diff HEAD')} before ` + 28 | `committing, so no accidental changes are made to your app.`); 29 | } 30 | 31 | function hasUncommittedChanges() { 32 | const statusOutput = shelljs.exec('git status --porcelain', { silent: true }).output; 33 | return (statusOutput !== ''); 34 | } 35 | 36 | function hasGeneratorRemote() { 37 | const remoteOutput = shelljs.exec('git remote -v', { silent: true }).output; 38 | 39 | let found = false; 40 | remoteOutput.split('\n').forEach(line => { 41 | if (line.indexOf('generator') === 0) { 42 | found = true; 43 | } 44 | }); 45 | 46 | return found; 47 | } 48 | -------------------------------------------------------------------------------- /other/webpack.config.es6.js: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * 3 | * WARNING: DO NOT EDIT THIS FILE! 4 | * 5 | * This is the standard webpack configuration for our team's apps. 6 | * 7 | * This configuration can be customized by passing configOptions. 8 | * 9 | * Read more on the wiki: 10 | * https://git.empdev.domo.com/AppTeam6/da-webpack/wiki/Webpack-Configuration 11 | * 12 | *****************************************************************************/ 13 | 14 | module.exports = function getConfig(configOptions) { 15 | 16 | // Handle options 17 | const INCLUDE_DESKTOP_VIEW = (configOptions.hasOwnProperty('includeDesktopView') ? configOptions.includeDesktopView : true); 18 | const INCLUDE_RESPONSIVE_VIEW = (configOptions.hasOwnProperty('includeResponsiveView') ? configOptions.includeResponsiveView : true); 19 | 20 | // dependencies 21 | const fs = require('fs'); 22 | const path = require('path'); 23 | const webpack = require('webpack'); 24 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 25 | const NgAnnotatePlugin = require('ng-annotate-webpack-plugin'); 26 | 27 | // postcss plugins 28 | const precss = require('precss'); 29 | const postcssImport = require('postcss-import'); 30 | const reporter = require('postcss-reporter'); 31 | const cssnano = require('cssnano'); 32 | const messages = require('postcss-browser-reporter'); 33 | const autoprefixer = require('autoprefixer'); 34 | 35 | // Environment 36 | const ON_DEV = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; 37 | const ON_TEST = process.env.NODE_ENV === 'test'; 38 | const ON_PROD = process.env.NODE_ENV === 'production'; 39 | const pkg = require('../package.json'); 40 | const bannerText = fs.readFileSync(path.resolve(__dirname, '../BANNER.txt')).toString(); 41 | 42 | const config = { 43 | cache: false, 44 | context: path.resolve(__dirname, '../src'), 45 | 46 | // We will add entry points based on the platform configs. 47 | entry: {}, 48 | 49 | // where 3rd-party modules can reside 50 | resolve: { 51 | modulesDirectories: ['node_modules', 'bower_components'] 52 | }, 53 | 54 | output: { 55 | // where to put standalone build file 56 | path: './dist', 57 | publicPath: '', 58 | filename: '/[name]/[name].js', 59 | sourceMapFilename: '[file].map', 60 | libraryTarget: 'umd' 61 | }, 62 | 63 | // dependencies listed here will NOT be bundled into the app, even if you `require` them. 64 | externals: { 65 | 'angular': { 66 | root: 'angular', 67 | commonjs: 'angular', 68 | commonjs2: 'angular', 69 | amd: 'angular' 70 | }, 71 | 'lodash': { 72 | root: '_', 73 | commonjs: 'lodash', 74 | commonjs2: 'lodash', 75 | amd: 'lodash' 76 | }, 77 | 'jquery': { 78 | root: '$', 79 | commonjs: 'jquery', 80 | commonjs2: 'jquery', 81 | amd: 'jQuery' 82 | }, 83 | 'd3': { 84 | root: 'd3', 85 | commonjs: 'd3', 86 | commonjs2: 'd3', 87 | amd: 'd3' 88 | } 89 | }, 90 | 91 | // optimization plugins 92 | // we add more items to this array based on configs set at top of file. 93 | plugins: [ 94 | new webpack.optimize.OccurenceOrderPlugin(), 95 | new webpack.ResolverPlugin( 96 | new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin('bower.json', ['main']) 97 | ), 98 | new webpack.optimize.DedupePlugin(), 99 | new NgAnnotatePlugin({ 100 | add: true, 101 | remove: false 102 | }), 103 | new webpack.BannerPlugin(bannerText), 104 | new webpack.DefinePlugin({ 105 | ON_DEV: ON_DEV, 106 | ON_TEST: ON_TEST, 107 | ON_PROD: ON_PROD 108 | }) 109 | ], 110 | 111 | // what loaders to use based on file type. 112 | module: { 113 | preLoaders: [ 114 | { 115 | test: /\.js$/, 116 | loader: 'eslint-loader', 117 | exclude: /(node_modules|bower_components)/, 118 | } 119 | ], 120 | loaders: [ 121 | { 122 | test: /\.js$/, 123 | exclude: /(node_modules|bower_components)/, 124 | loader: 'babel', 125 | query: { 126 | cacheDirectory: true, 127 | presets: ['es2015-loose', 'stage-1'], 128 | plugins: ['transform-runtime', 'add-module-exports'] 129 | } 130 | }, 131 | { 132 | test: /\.css$/, 133 | loader: 'style!css!postcss', 134 | exclude: /(node_modules|bower_components)/ 135 | }, 136 | { 137 | test: /\.(png|jpeg|gif).*$/, 138 | loader: 'file?name=/[name].[ext]?[hash]' 139 | }, 140 | { 141 | test: /\.html$/, 142 | loader: 'raw' 143 | }, 144 | { 145 | test: /\.(woff|ttf|eot|svg).*$/, 146 | loader: 'file?name=/[name].[ext]?[hash]' 147 | } 148 | ] 149 | }, 150 | 151 | // postcss plugins settings 152 | postcss: function postcss(_webpack) { 153 | const postcssPlugins = [ 154 | postcssImport({ 155 | addDependencyTo: _webpack, 156 | onImport: function onImport(files) { 157 | files.forEach(this.addDependency); 158 | }.bind(this) 159 | }), 160 | precss(), 161 | autoprefixer({ browsers: ['last 2 versions'] }), 162 | reporter() 163 | ]; 164 | // only minify when on production 165 | if (ON_PROD) { 166 | postcssPlugins.push(cssnano({ 167 | mergeRules: false, 168 | zindex: false, 169 | reduceIdents: false, 170 | mergeIdents: false 171 | })); 172 | } else { 173 | // use the message reported when on development 174 | postcssPlugins.push(messages()); 175 | } 176 | 177 | return postcssPlugins; 178 | }, 179 | 180 | eslint: { 181 | formatter: require('eslint-friendly-formatter'), 182 | }, 183 | 184 | devtool: 'source-map', 185 | 186 | devServer: { 187 | contentBase: 'dist/', 188 | noInfo: false, 189 | hot: false, 190 | inline: false 191 | } 192 | }; 193 | 194 | /** 195 | * If on production then minify code else (on dev) and turn on hot module replacement. 196 | */ 197 | if (ON_PROD) { 198 | config.plugins.push(new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })); 199 | } else { 200 | config.plugins.push(new webpack.HotModuleReplacementPlugin()); 201 | } 202 | 203 | 204 | /** 205 | * Logic to change build based on configs at top of file. 206 | */ 207 | const INCLUDE_MULTIPLE_VIEWS = INCLUDE_DESKTOP_VIEW && INCLUDE_RESPONSIVE_VIEW; 208 | if (!INCLUDE_DESKTOP_VIEW && !INCLUDE_RESPONSIVE_VIEW) { 209 | throw new Error('You must include at least one view!'); 210 | } 211 | /** 212 | * Setup the desktop view if INCLUDE_DESKTOP_VIEW is set to true 213 | */ 214 | if (INCLUDE_DESKTOP_VIEW) { 215 | config.entry.desktop = './desktop/index.js'; 216 | if (!ON_TEST) { 217 | config.plugins.push( 218 | new HtmlWebpackPlugin({ 219 | title: 'Desktop', 220 | dev: ON_DEV, 221 | pkg: pkg, 222 | template: 'src/desktop/desktop.html', // Load a custom template 223 | inject: 'body', // Inject all scripts into the body 224 | filename: INCLUDE_MULTIPLE_VIEWS ? 'desktop/index.html' : 'index.html', 225 | chunks: ['desktop'] 226 | }) 227 | ); 228 | } 229 | } 230 | /** 231 | * Setup the responsive view if INCLUDE_RESPONSIVE_VIEW is set to true 232 | */ 233 | if (INCLUDE_RESPONSIVE_VIEW) { 234 | config.entry.responsive = './responsive/index.js'; 235 | if (!ON_TEST) { 236 | config.plugins.push( 237 | new HtmlWebpackPlugin({ 238 | title: 'Responsive', 239 | dev: ON_DEV, 240 | pkg: pkg, 241 | template: 'src/responsive/responsive.html', // Load a custom template 242 | inject: 'body', // Inject all scripts into the body 243 | filename: INCLUDE_MULTIPLE_VIEWS ? 'responsive/index.html' : 'index.html', 244 | chunks: ['responsive'] 245 | }) 246 | ); 247 | } 248 | } 249 | /** 250 | * add swither and lab view when app has multiple views. 251 | * INCLUDE_MULTIPLE_VIEWS is true when INCLUDE_DESKTOP_VIEW and INCLUDE_RESPONSIVE_VIEW views are both true 252 | */ 253 | if (INCLUDE_MULTIPLE_VIEWS) { 254 | config.entry.switcher = './switcher.js'; 255 | if (!ON_TEST) { 256 | config.plugins.push( 257 | new HtmlWebpackPlugin({ 258 | title: 'Switcher', 259 | dev: ON_DEV, 260 | pkg: pkg, 261 | chunks: ['switcher'] 262 | }), 263 | new HtmlWebpackPlugin({ 264 | title: 'Lab', 265 | dev: ON_DEV, 266 | pkg: pkg, 267 | template: 'lab.html', 268 | filename: 'lab.html', 269 | chunks: [] 270 | }) 271 | ); 272 | } 273 | } 274 | 275 | /** 276 | * Add any extra externals 277 | */ 278 | if (configOptions.hasOwnProperty('externals')) { 279 | const addedExternals = configOptions.externals; 280 | for (const key in addedExternals) { 281 | if (addedExternals.hasOwnProperty(key)) { 282 | config.externals[key] = addedExternals[key]; 283 | } 284 | } 285 | } 286 | 287 | /** 288 | * Add any extra loaders 289 | */ 290 | if (configOptions.hasOwnProperty('loaders') && configOptions.loaders.length > 0) { 291 | const addedLoaders = configOptions.loaders; 292 | config.module.loaders.push.apply(config.module.loaders, addedLoaders); 293 | } 294 | 295 | /** 296 | * Return the finished configuration back to the caller 297 | */ 298 | return config; 299 | 300 | }; 301 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-app", 3 | "version": "0.1.2", 4 | "description": "A sample app created with starter-kit", 5 | "scripts": { 6 | "setup": "node other/setup.js", 7 | "start": "NODE_ENV=development node server.js", 8 | "test-ci": "npm run lint && NODE_ENV=test karma start ./karma.conf.js --single-run --browsers PhantomJS", 9 | "test": "npm run lint && NODE_ENV=test karma start ./karma.conf.js --single-run", 10 | "tdd": "NODE_ENV=test karma start ./karma.conf.js --browsers Chrome", 11 | "lint": "eslint src/**", 12 | "coveralls": "npm run test-ci && node node_modules/lcov-filter/index.js ./coverage/phantomjs/lcov.info spec | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 13 | "build": "rm -rf ./dist && NODE_ENV=production ./node_modules/.bin/webpack && cp -r domo/* dist/", 14 | "preversion": "npm run test-ci", 15 | "version": "npm run build && npm run changelog && git add -A CHANGELOG.md", 16 | "postversion": "git push && git push --tags", 17 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -w -r 0", 18 | "upload": "npm run build && cd dist/ && domo publish && cd ..", 19 | "update-tools": "node other/update.js" 20 | }, 21 | "author": "AppTeam6 ", 22 | "repository": "https://github.com/DomoApps/sample-app.git", 23 | "license": "SEE LICENSE IN LICENSE", 24 | "devDependencies": { 25 | "@domoinc/ca-icon-trends-with-text": "^6.0.6", 26 | "@domoinc/da-plop": "^3.0.0", 27 | "@domoinc/multi-line-chart": "^4.0.11", 28 | "@domoinc/query": "^1.0.3", 29 | "angular-animate": "^1.5.7", 30 | "angular-aria": "^1.5.7", 31 | "angular-material": "^1.1.0-rc.5", 32 | "autoprefixer": "^6.1.2", 33 | "babel-core": "^6.3.17", 34 | "babel-loader": "^6.2.0", 35 | "babel-plugin-add-module-exports": "^0.1.1", 36 | "babel-plugin-closure-elimination": "0.0.2", 37 | "babel-plugin-transform-runtime": "^6.3.13", 38 | "babel-preset-es2015-loose": "^6.1.3", 39 | "babel-preset-stage-1": "^6.3.13", 40 | "babel-register": "^6.3.13", 41 | "babel-runtime": "^6.3.13", 42 | "body-parser": "^1.15.1", 43 | "cdnjs": "^0.3.2", 44 | "chai": "^3.4.1", 45 | "chalk": "^1.1.1", 46 | "conventional-changelog": "^0.5.3", 47 | "coveralls": "^2.11.6", 48 | "css-loader": "^0.23.0", 49 | "cssnano": "^3.4.0", 50 | "cz-conventional-changelog": "^1.1.5", 51 | "eslint": "^1.10.3", 52 | "eslint-config-airbnb": "^2.0.0", 53 | "eslint-friendly-formatter": "^1.2.2", 54 | "eslint-loader": "^1.1.1", 55 | "express": "^4.13.3", 56 | "file-loader": "^0.8.5", 57 | "fs-extra": "^0.26.2", 58 | "glob": "^6.0.1", 59 | "html-webpack-plugin": "^1.7.0", 60 | "inquirer": "^0.11.4", 61 | "install": "^0.4.1", 62 | "istanbul": "^0.4.1", 63 | "istanbul-instrumenter-loader": "^0.1.3", 64 | "json-loader": "^0.5.4", 65 | "karma": "^0.13.15", 66 | "karma-babel-preprocessor": "^6.0.1", 67 | "karma-chai": "^0.1.0", 68 | "karma-chrome-launcher": "^0.2.2", 69 | "karma-coverage": "^0.5.3", 70 | "karma-firefox-launcher": "^0.1.7", 71 | "karma-mocha": "^0.2.1", 72 | "karma-phantomjs-launcher": "^1.0.0", 73 | "karma-safari-launcher": "^0.1.1", 74 | "karma-sinon": "^1.0.4", 75 | "karma-webpack": "^1.7.0", 76 | "lcov-filter": "0.1.1", 77 | "lodash.kebabcase": "^3.1.1", 78 | "mocha": "^2.3.4", 79 | "moment": "^2.13.0", 80 | "ng-annotate-webpack-plugin": "^0.1.2", 81 | "npm": "^3.5.2", 82 | "phantomjs-prebuilt": "^2.1.7", 83 | "portfinder": "^0.4.0", 84 | "postcss-browser-reporter": "^0.4.0", 85 | "postcss-import": "^7.1.3", 86 | "postcss-loader": "^0.8.0", 87 | "postcss-reporter": "^1.3.0", 88 | "precss": "^1.3.0", 89 | "progress": "^1.1.8", 90 | "raw-loader": "^0.5.1", 91 | "request": "^2.67.0", 92 | "ryuu-client": "^2.5.0", 93 | "shelljs": "^0.5.3", 94 | "sinon": "^1.17.2", 95 | "style-loader": "^0.13.0", 96 | "webpack": "^1.12.9", 97 | "webpack-dev-middleware": "^1.4.0", 98 | "webpack-dev-server": "^1.14.0" 99 | }, 100 | "dependencies": { 101 | "enquire.js": "^2.1.1", 102 | "ryuu.js": "2.5.4" 103 | }, 104 | "config": { 105 | "AppTeam6": { 106 | "framework": "da-webpack", 107 | "frameworkVersion": "0.0.5", 108 | "features": [] 109 | }, 110 | "commitizen": { 111 | "path": "./node_modules/cz-conventional-changelog" 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@domoinc/da-plop'); 2 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | module.exports = require('./other/server.es6'); 3 | -------------------------------------------------------------------------------- /src/common/components/README.md: -------------------------------------------------------------------------------- 1 | - Place components that we could potentially pull out into da-bits or other shared code here 2 | - These are dumb components that aren't tied to specific data queries, etc. 3 | - For loading purposes: don't include anything you don't intend to use on both platforms -------------------------------------------------------------------------------- /src/common/filters/README.md: -------------------------------------------------------------------------------- 1 | Place Angular filter objects here. -------------------------------------------------------------------------------- /src/common/filters/default/default.filter.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | /** 3 | * filter that will revert to a 'default' text when a value is undefined 4 | * 5 | * @param {string} def default text 6 | * @return {string} either the input value or the `def` param 7 | */ 8 | function defaultFilter() { 9 | return (value, def) => { 10 | return (typeof value === 'undefined' ? def : value); 11 | }; 12 | } 13 | 14 | // inject dependencies here 15 | defaultFilter.$inject = []; 16 | 17 | ngModule.filter('default', defaultFilter); 18 | 19 | if (ON_TEST) { 20 | require('./default.filter.spec.js')(ngModule); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/common/filters/default/default.filter.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('Filter:default', () => { 3 | let $injector; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(inject((_$injector_) => { 8 | $injector = _$injector_; 9 | })); 10 | 11 | it('should test properly', () => { 12 | expect($injector.has('defaultFilter')).to.not.equal(false); 13 | }); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/filters/summary/summary.filter.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | /** 3 | * summary filter will summarize numbers. 5000 => 5K, 10000000 => 10M 4 | * @param {bool/undefined} optFractional if set to true, will assume that quantities are not discrete 5 | * i.e., 'dollars' should set fractional to true, while 6 | * 'products' should not 7 | * @param {number/undefined} optPrecision floating point precision of returned number 8 | * @return {string} 9 | */ 10 | function summary() { 11 | const units = ['K', 'M', 'B', 'T']; 12 | return (input, optFractional, optPrecision) => { 13 | const fractional = (typeof optFractional !== 'undefined' ? optFractional : false); 14 | const precision = (typeof optPrecision !== 'undefined' ? optPrecision : 0); 15 | if (typeof input === 'undefined' || isNaN(input)) { 16 | return input; 17 | } 18 | for (let i = units.length - 1; i >= 0; i--) { 19 | const decimal = Math.pow(1000, i + 1); 20 | 21 | if (input <= -decimal || input >= decimal) { 22 | return (input / decimal).toFixed(precision) + units[i]; 23 | } 24 | } 25 | if (fractional) { 26 | return input.toFixed(precision); 27 | } 28 | return input; 29 | }; 30 | } 31 | 32 | // inject dependencies here 33 | summary.$inject = []; 34 | 35 | ngModule.filter('summary', summary); 36 | 37 | if (ON_TEST) { 38 | require('./summary.filter.spec.js')(ngModule); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/common/filters/summary/summary.filter.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('Filter:summary', () => { 3 | let $injector; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(inject((_$injector_) => { 8 | $injector = _$injector_; 9 | })); 10 | 11 | it('should test properly', () => { 12 | expect($injector.has('summaryFilter')).to.not.equal(false); 13 | }); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/index.js: -------------------------------------------------------------------------------- 1 | require('./styles/typebase.css'); 2 | 3 | import angular from 'angular'; 4 | import { attachAll } from '../../other/boilerplate-utils.js'; 5 | const ngModule = angular.module('da.common', []); 6 | 7 | attachAll(require.context('./services', true, /\.factory\.js$/))(ngModule); 8 | attachAll(require.context('./services', true, /\.value\.js$/))(ngModule); 9 | attachAll(require.context('./filters', true, /\.filter\.js$/))(ngModule); 10 | attachAll(require.context('./components', true, /\.(component|directive)\.js$/))(ngModule); 11 | 12 | export default ngModule; 13 | -------------------------------------------------------------------------------- /src/common/services/da-events/da-events.factory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * da-events is included in starter-kit 3 | * it's a simple event listener that can register and trigger callbacks 4 | * based on events that are triggered through its methods 5 | */ 6 | module.exports = ngModule => { 7 | function daEvents($rootScope, $q, $log, SAMPLE_APP) { 8 | // central place for documenting app events 9 | const _eventRegistry = { 10 | 'app:loaded': 'This event is fired when the appFrame as finished loading.', 11 | [SAMPLE_APP.E_CAT_FILTER_CHANGE]: 'This even is fired when the filters get updated' 12 | }; 13 | 14 | // Create promise to reolve when appFrame has finished animating 15 | const _diferred = $q.defer(); 16 | const offFn = $rootScope.$on('app:loaded', () => { 17 | _diferred.resolve('The app has loaded!'); 18 | // This will un-register the event listener once its happend. 19 | offFn(); 20 | }); 21 | 22 | // Public API here 23 | const api = { 24 | /** 25 | * Registers an event listener if it is defined in `_eventRegistry` object. 26 | * @param {string} name Name of event. Must match key in `_eventRegistry` object. 27 | * @param {func} listener The function to be called when event is triggered. First param is $event obj. 28 | * @return {unbind} Returns the unbind function for use of destroying event. 29 | */ 30 | on(name, listener) { 31 | if (_eventRegistry.hasOwnProperty(name)) { 32 | return $rootScope.$on(name, listener); 33 | } 34 | $log.error('No event registered with name: ' + name); 35 | return null; 36 | }, 37 | /** 38 | * Triggers an event that has been defined in `eventRegistry` object. 39 | * @param {string} name Name of event to trigger. Must be defined first. 40 | * @param {[.]} args Args to pass to callback. 41 | * @return {[type]} [description] 42 | */ 43 | trigger(name, args) { 44 | if (_eventRegistry.hasOwnProperty(name)) { 45 | return $rootScope.$emit(name, args); 46 | } 47 | $log.error('No event registered with name: ' + name); 48 | return null; 49 | }, 50 | }; 51 | 52 | // promise that is resolved when 'app:loaded' event is fired. 53 | api.appLoadPromise = _diferred.promise; 54 | 55 | return api; 56 | } 57 | 58 | daEvents.$inject = ['$rootScope', '$q', '$log', 'SAMPLE_APP']; 59 | 60 | ngModule.factory('daEvents', daEvents); 61 | 62 | if (ON_TEST) { 63 | require('./da-events.factory.spec.js')(ngModule); 64 | } 65 | 66 | return ngModule; 67 | }; 68 | -------------------------------------------------------------------------------- /src/common/services/da-events/da-events.factory.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Here you can write tests for you service 3 | * @param {Angular Module} ngModule The module with the service 4 | */ 5 | module.exports = ngModule => { 6 | describe('factory:daEvents', () => { 7 | let daEvents; 8 | 9 | beforeEach(window.module(ngModule.name)); 10 | 11 | beforeEach(() => { 12 | window.module(($provide) => { 13 | $provide.factory('SAMPLE_APP', () => ({})); 14 | }); 15 | 16 | inject(_daEvents_ => { 17 | daEvents = _daEvents_; 18 | }); 19 | }); 20 | 21 | it('should exist emit registered events', () => { 22 | const spy = sinon.spy(); 23 | daEvents.on('app:loaded', spy); 24 | daEvents.trigger('app:loaded'); 25 | expect(spy.calledOnce).to.equal(true); 26 | }); 27 | 28 | it('should not allow a listener to be setup for event that is not in registry', () => { 29 | const spy = sinon.spy(); 30 | daEvents.on('not:in:registry', spy); 31 | daEvents.trigger('not:in:registry'); 32 | expect(spy.calledOnce).to.equal(false); 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/common/services/date-range-items/date-range-items.value.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | const ranges = [ 3 | { 4 | name: 'All Time', 5 | value: 'all' 6 | }, 7 | { 8 | name: 'Last Year', 9 | value: 'year' 10 | }, 11 | { 12 | name: 'This Quarter Last Year', 13 | value: 'quarter' 14 | } 15 | ]; 16 | 17 | ngModule.value('dateRangeItems', ranges); 18 | 19 | if (ON_TEST) { 20 | require('./date-range-items.value.spec.js')(ngModule); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/common/services/date-range-items/date-range-items.value.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('value:dateRangeItems', () => { 3 | let dateRangeItems; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(() => { 8 | inject(_dateRangeItems_ => { 9 | dateRangeItems = _dateRangeItems_; 10 | }); 11 | }); 12 | 13 | it('should test properly', () => { 14 | expect(dateRangeItems).to.not.equal(undefined); 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/services/global-filters-factory/global-filters-factory.factory.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | /** 3 | * a simple wrapper for da-events to facilitate global filter changes 4 | */ 5 | function globalFiltersFactory(daEvents, SAMPLE_APP) { 6 | // Private variables 7 | let _filter = SAMPLE_APP.DEFAULT_CATEGORY; 8 | 9 | // Public API here 10 | const service = { 11 | setFilter, 12 | getFilter, 13 | onFilterChange 14 | }; 15 | 16 | return service; 17 | 18 | //// Functions //// 19 | /** 20 | * will set the current filter and trigger all callbacks with the new _filter 21 | * @param {string} newFilter the new filter text to use 22 | */ 23 | function setFilter(newFilter) { 24 | if ((typeof newFilter === 'string') && (newFilter !== _filter)) { 25 | _filter = newFilter; 26 | daEvents.trigger(SAMPLE_APP.E_CAT_FILTER_CHANGE, newFilter); 27 | return newFilter; 28 | } 29 | } 30 | 31 | /** 32 | * @return {string} the current filter 33 | */ 34 | function getFilter() { 35 | return _filter; 36 | } 37 | 38 | /** 39 | * registers a callback that will be called every time the filter is changed 40 | * @param {Function} callback a function that is passed (event object, newFilter) 41 | */ 42 | function onFilterChange(callback) { 43 | daEvents.on(SAMPLE_APP.E_CAT_FILTER_CHANGE, callback); 44 | } 45 | } 46 | 47 | // inject dependencies here 48 | globalFiltersFactory.$inject = ['daEvents', 'SAMPLE_APP']; 49 | 50 | ngModule.factory('globalFiltersFactory', globalFiltersFactory); 51 | 52 | if (ON_TEST) { 53 | require('./global-filters-factory.factory.spec.js')(ngModule); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/common/services/global-filters-factory/global-filters-factory.factory.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('factory:globalFiltersFactory', () => { 3 | let globalFiltersFactory; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(() => { 8 | window.module(($provide) => { 9 | $provide.factory('daEvents', () => ({})); 10 | $provide.factory('SAMPLE_APP', () => ({})); 11 | }); 12 | inject(_globalFiltersFactory_ => { 13 | globalFiltersFactory = _globalFiltersFactory_; 14 | }); 15 | }); 16 | 17 | it('should test properly', () => { 18 | expect(globalFiltersFactory).to.not.equal(undefined); 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/common/services/granularity-items/granularity-items.value.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | const granularities = ['month', 'week', 'quarter']; 3 | 4 | ngModule.value('granularityItems', granularities); 5 | 6 | if (ON_TEST) { 7 | require('./granularity-items.value.spec.js')(ngModule); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/common/services/granularity-items/granularity-items.value.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('value:granularityItems', () => { 3 | let granularityItems; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(() => { 8 | inject(_granularityItems_ => { 9 | granularityItems = _granularityItems_; 10 | }); 11 | }); 12 | 13 | it('should test properly', () => { 14 | expect(granularityItems).to.not.equal(undefined); 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/services/product-table-header/product-table-header.value.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | const headerInfo = [ 3 | [ 4 | { 5 | width: 25, 6 | title: 'In Stock', 7 | key: 'inStock' 8 | }, 9 | { 10 | width: 75, 11 | title: 'Name', 12 | key: 'name' 13 | } 14 | ], 15 | [ 16 | { 17 | width: 33, 18 | title: 'Price', 19 | key: 'price' 20 | }, 21 | { 22 | width: 33, 23 | title: 'Category', 24 | key: 'category' 25 | }, 26 | { 27 | width: 33, 28 | title: 'Quantity', 29 | key: 'quantity' 30 | } 31 | ] 32 | ]; 33 | 34 | ngModule.value('productTableHeader', headerInfo); 35 | 36 | if (ON_TEST) { 37 | require('./product-table-header.value.spec.js')(ngModule); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/common/services/product-table-header/product-table-header.value.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('value:productTableHeader', () => { 3 | let productTableHeader; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(() => { 8 | inject(_productTableHeader_ => { 9 | productTableHeader = _productTableHeader_; 10 | }); 11 | }); 12 | 13 | it('should test properly', () => { 14 | expect(productTableHeader).to.not.equal(undefined); 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/services/products-factory/dev-products-factory.factory.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | function devProductsFactory(SAMPLE_APP, $timeout) { 3 | const sampleProducts = require('../../../../data/sample-products.json'); 4 | // Private variables 5 | const _productsPromises = {}; 6 | // Public API here 7 | const service = { 8 | getProducts, 9 | getProductCategories, 10 | getInventoryValue, 11 | getNumUniqueProducts, 12 | getTotalQuantity 13 | }; 14 | 15 | return service; 16 | 17 | //// Functions //// 18 | 19 | /** 20 | * returns a list of products form the server. 21 | * @param {string} category optional: string of the category to filter by 22 | * @return {promise} Promise returning an array of format [{category, name, price, quantity}, {...}, ...] 23 | */ 24 | function getProducts(optCategory) { 25 | const category = typeof optCategory === 'undefined' ? SAMPLE_APP.DEFAULT_CATEGORY : optCategory; 26 | 27 | // check to make sure this request hasn't already been filled 28 | if (typeof _productsPromises[category] !== 'undefined') { 29 | return _productsPromises[category]; 30 | } 31 | 32 | // store productsPromise in case a parallel request comes in, that way the data is requested only once 33 | // timeout is to mock the time it takes for a real request 34 | _productsPromises[category] = $timeout(() => { 35 | return (category === SAMPLE_APP.DEFAULT_CATEGORY) ? sampleProducts : sampleProducts.filter(product => { 36 | return product.category === category; 37 | }); 38 | }, 1000); 39 | return _productsPromises[category]; 40 | } 41 | 42 | /** 43 | * returns a list of product categories 44 | * @return {promise} Promise returning array of format [string, string, string...] 45 | */ 46 | function getProductCategories() { 47 | return getProducts().then(productsArray => { 48 | const categories = {}; 49 | productsArray.forEach(product => { 50 | categories[product.category] = true; 51 | }); 52 | return Object.keys(categories); 53 | }); 54 | } 55 | 56 | /** 57 | * returns a number representing the total value of the products 58 | * @return {promise(number)} total value of products 59 | */ 60 | function getInventoryValue(category) { 61 | return getProducts(category).then(productsArray => { 62 | return productsArray.reduce((totalValue, product) => { 63 | return totalValue + (product.price * product.quantity); 64 | }, 0); 65 | }); 66 | } 67 | 68 | /** 69 | * returns a number representing the amount of unique product types 70 | * @return {promise(number)} number of unique product types 71 | */ 72 | function getNumUniqueProducts(category) { 73 | return getProducts(category).then(productsArray => { 74 | return productsArray.length; 75 | }); 76 | } 77 | 78 | /** 79 | * returns a number representing the total number of physical products 80 | * @return {promise(number)} number of physical products 81 | */ 82 | function getTotalQuantity(category) { 83 | return getProducts(category).then(productsArray => { 84 | return productsArray.reduce((totalProductQuantity, product) => { 85 | return totalProductQuantity + product.quantity; 86 | }, 0); 87 | }); 88 | } 89 | } 90 | 91 | devProductsFactory.$inject = ['SAMPLE_APP', '$timeout']; 92 | 93 | ngModule.factory('devProductsFactory', devProductsFactory); 94 | }; 95 | -------------------------------------------------------------------------------- /src/common/services/products-factory/prod-products-factory.factory.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | function prodProductsFactory(SAMPLE_APP) { 3 | const domo = require('ryuu.js'); 4 | const Query = require('@domoinc/query'); 5 | // Private variables 6 | const _productsPromises = {}; 7 | // Public API here 8 | const service = { 9 | getProducts, 10 | getProductCategories, 11 | getInventoryValue, 12 | getNumUniqueProducts, 13 | getTotalQuantity 14 | }; 15 | 16 | return service; 17 | 18 | 19 | /** 20 | * returns a list of products form the server. 21 | * @param {string} category optional: string of the category to filter by 22 | * @return {promise} Promise returning an array of format [{category, name, price, quantity}, {...}, ...] 23 | */ 24 | function getProducts(optCategory) { 25 | const category = typeof optCategory === 'undefined' ? SAMPLE_APP.DEFAULT_CATEGORY : optCategory; 26 | 27 | // check to make sure this request hasn't already been filled 28 | if (typeof _productsPromises[category] !== 'undefined') { 29 | return _productsPromises[category]; 30 | } 31 | 32 | // store productsPromise in case a parallel request comes in, that way the data is requested only once 33 | const productsQuery = (new Query()).select(['category', 'name', 'price', 'quantity']); 34 | if (category !== SAMPLE_APP.DEFAULT_CATEGORY) { 35 | productsQuery.where('category').equals(category); 36 | } 37 | _productsPromises[category] = domo.get(productsQuery.query('products')); 38 | return _productsPromises[category]; 39 | } 40 | 41 | /** 42 | * returns a list of product categories 43 | * @return {promise} Promise returning array of format [string, string, string...] 44 | */ 45 | function getProductCategories() { 46 | return domo.get((new Query()) 47 | .select(['category']) 48 | .groupBy('category') 49 | .query('products')) 50 | .then(categories => { 51 | return categories.map(category => { 52 | return category.category; 53 | }); 54 | }); 55 | } 56 | 57 | /** 58 | * returns a number representing the total value of the products 59 | * @return {promise(number)} total value of products 60 | */ 61 | function getInventoryValue(category) { 62 | return getProducts(category).then(productsArray => { 63 | return productsArray.reduce((totalValue, product) => { 64 | return totalValue + (product.price * product.quantity); 65 | }, 0); 66 | }); 67 | } 68 | 69 | /** 70 | * returns a number representing the amount of unique product types 71 | * @return {promise(number)} number of unique product types 72 | */ 73 | function getNumUniqueProducts(category) { 74 | const query = (new Query()).select(['name']); 75 | if (typeof category !== 'undefined' && category !== SAMPLE_APP.DEFAULT_CATEGORY) { 76 | query.where('category').equals(category); 77 | } 78 | return domo.get(query 79 | .select(['name']) 80 | .aggregate('name', 'count') 81 | .query('products')) 82 | .then(result => { 83 | return result[0].name; 84 | }); 85 | } 86 | 87 | /** 88 | * returns a number representing the total number of physical products 89 | * @return {promise(number)} number of physical products 90 | */ 91 | function getTotalQuantity(category) { 92 | const query = (new Query()).select(['quantity']); 93 | if (typeof category !== 'undefined' && category !== SAMPLE_APP.DEFAULT_CATEGORY) { 94 | query.where('category').equals(category); 95 | } 96 | return domo.get(query 97 | .select(['name']) 98 | .aggregate('quantity', 'sum') 99 | .query('products')) 100 | .then(result => { 101 | return result[0].quantity; 102 | }); 103 | } 104 | } 105 | 106 | prodProductsFactory.$inject = ['SAMPLE_APP']; 107 | 108 | ngModule.factory('prodProductsFactory', prodProductsFactory); 109 | }; 110 | -------------------------------------------------------------------------------- /src/common/services/products-factory/products-factory.factory.js: -------------------------------------------------------------------------------- 1 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2 | // !!!!!! Note about datasources !!!!!!! 3 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 4 | // 5 | // Normally in a production app one would 6 | // gather data from a 'datasource'. This 7 | // data is gathered through the Domo client 8 | // included by adding `domo = require('ryuu.js');` 9 | // to the top of your file. 10 | // 11 | // For our purposes we have left sample code 12 | // using the Domo client to query data. 13 | // The code you use can be chosen by changing 14 | // the value of MOCK_REQUESTS in desktop/index.js 15 | // 16 | // More information on the Domo client can 17 | // be found at https://developer.domo.com 18 | /** 19 | * productsService: interface for domo backend 20 | * @method getProducts 21 | * @method getProductCategories 22 | * @method getInventoryValue 23 | * @method getNumUniqueProducts 24 | * @method getTotalQuantity 25 | */ 26 | module.exports = ngModule => { 27 | function productsFactory(SAMPLE_APP, $injector) { 28 | if (SAMPLE_APP.MOCK_REQUESTS) { 29 | return $injector.get('devProductsFactory'); 30 | } 31 | return $injector.get('prodProductsFactory'); 32 | } 33 | 34 | // inject dependencies here 35 | productsFactory.$inject = ['SAMPLE_APP', '$injector']; 36 | 37 | ngModule.factory('productsFactory', productsFactory); 38 | 39 | if (ON_TEST) { 40 | require('./products-factory.factory.spec.js')(ngModule); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/common/services/products-factory/products-factory.factory.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('factory:productsFactory', () => { 3 | let productsFactory; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(() => { 8 | window.module(($provide) => { 9 | $provide.factory('SAMPLE_APP', () => ({})); 10 | }); 11 | inject(_productsFactory_ => { 12 | productsFactory = _productsFactory_; 13 | }); 14 | }); 15 | 16 | it('should test properly', () => { 17 | expect(productsFactory).to.not.equal(undefined); 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/services/transaction-pill-data-factory/transaction-pill-data.factory.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | function transactionPillDataFactory($mdColors) { 3 | const pilldata = [ 4 | { 5 | text: 'Total Income', 6 | summary: null, 7 | chart: null, 8 | color: $mdColors.getThemeColor('default-domoPrimary-700') 9 | }, 10 | { 11 | text: 'Products Sold', 12 | summary: null, 13 | chart: null, 14 | color: $mdColors.getThemeColor('default-domoAccent-A200') 15 | }, 16 | { 17 | text: 'Transactions', 18 | summary: null, 19 | chart: null, 20 | color: $mdColors.getThemeColor('default-domoWarn-600') 21 | } 22 | ]; 23 | 24 | return { 25 | getPillData, 26 | }; 27 | 28 | function getPillData() { 29 | return pilldata; 30 | } 31 | } 32 | 33 | // inject dependencies here 34 | transactionPillDataFactory.$inject = ['$mdColors']; 35 | 36 | ngModule.factory('transactionPillDataFactory', transactionPillDataFactory); 37 | 38 | if (ON_TEST) { 39 | require('./transaction-pill-data.factory.spec.js')(ngModule); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/common/services/transaction-pill-data-factory/transaction-pill-data.factory.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('factory:transactionPillDataFactory', () => { 3 | let transactionPillDataFactory; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(() => { 8 | window.module(($provide) => { 9 | $provide.factory('$mdColors', () => { 10 | return { 11 | getThemeColor: () => { 12 | return '#fff'; 13 | } 14 | }; 15 | }); 16 | }); 17 | inject(_transactionPillDataFactory_ => { 18 | transactionPillDataFactory = _transactionPillDataFactory_; 19 | }); 20 | }); 21 | 22 | it('should test properly', () => { 23 | expect(transactionPillDataFactory).to.not.equal(undefined); 24 | }); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/common/services/transactions-analytics-factory/dev-transactions-analytics-factory.factory.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | function devTransactionsAnalyticsFactory($q, SAMPLE_APP, $timeout) { 3 | // moment library for date formatting 4 | // needs to be instantiated with moment().format() 5 | const moment = require('moment'); 6 | moment().format(); 7 | 8 | const sampleTransactions = require('../../../../data/sample-transactions.json'); 9 | 10 | // Public API here 11 | const service = { 12 | getTotals, 13 | getTransactionsPerX 14 | }; 15 | 16 | return service; 17 | 18 | //// Functions //// 19 | /** 20 | * gets useful totals for a certain category or date range 21 | * 22 | * @param {string} categoryFilter 23 | * @param {string or object or undefined} dateRangeFilter - either 'year' for last year, 'quarter' 24 | **for same quarter last year, or undefined 25 | * @return {promise} - promise returning object of format {transactionCount: X, productsSold: X, income: X} 26 | */ 27 | function getTotals(categoryFilter, dateRangeFilter) { 28 | let totals = sampleTransactions; 29 | if (categoryFilter !== SAMPLE_APP.DEFAULT_CATEGORY) { 30 | totals = totals.filter(transaction => { return transaction.category === categoryFilter; }); 31 | } 32 | const lastYear = moment().subtract(1, 'years'); 33 | const beginQuarter = moment().subtract(1, 'years').startOf('quarter'); 34 | const endQuarter = moment().subtract(1, 'years').endOf('quarter'); 35 | totals = totals.filter(transaction => { 36 | if (dateRangeFilter === 'year') { 37 | return moment(transaction.date).isSame(lastYear, 'year'); 38 | } 39 | if (dateRangeFilter === 'quarter') { 40 | return moment(transaction.date).isBetween(beginQuarter, endQuarter, 'day', '[]'); 41 | } 42 | return true; 43 | }); 44 | return $timeout(() => { 45 | return totals.reduce((accumulated, currentRow) => { 46 | accumulated.transactionCount++; 47 | accumulated.productsSold += currentRow.quantity; 48 | accumulated.income += currentRow.total; 49 | return accumulated; 50 | }, { transactionCount: 0, productsSold: 0, income: 0 }); 51 | }, 1000); 52 | } 53 | 54 | /** 55 | * gets data on transactions per dateGrain (month, week, etc...) 56 | * @param {string} categoryFilter 57 | * @param {string/undefined} optDateGrain String of either 'month', 'week', or 'quarter'. defaults to month 58 | * @param {object/string/undefined} dateRangeFilter 'year' for last year, 'quarter' for this quarter last year, or undefined 59 | * @return {array[objects]} array of format [{date: string, total: number, quantity: number, category: number}, ...] 60 | */ 61 | function getTransactionsPerX(categoryFilter, optDateGrain, optDateRange) { 62 | // because this is a sample app we will modify the JSON ourselves 63 | // readability was chosen over performance 64 | return $timeout(() => { 65 | return $q(resolve => { 66 | let transactions = sampleTransactions; 67 | const dateGrain = (typeof optDateGrain !== 'undefined' ? optDateGrain : 'month'); 68 | transactions = _applyCategoryFilter(transactions, categoryFilter); 69 | transactions = transactions.map(transaction => { 70 | // clone object so we don't mutate our sampleTransactions and convert string dates to moments 71 | return Object.assign({}, transaction, { date: moment(transaction.date) }); 72 | }); 73 | if (typeof optDateRange !== 'undefined') { 74 | transactions = _applyDateRangeFilter(transactions, optDateRange); 75 | } 76 | transactions.sort((a, b) => { 77 | return a.date - b.date; 78 | }); 79 | transactions = _applyDateGrainFilter(transactions, dateGrain); 80 | transactions = transactions.map(transaction => { 81 | transaction.date = transaction.date.format('YYYY-MM-DD'); 82 | return transaction; 83 | }); 84 | resolve(transactions); 85 | }); 86 | }, 1000); 87 | } 88 | 89 | function _applyDateGrainFilter(query, dateGrain) { 90 | return query.reduce((accumulated, currentRow) => { 91 | // mutates currentRow and accumulated 92 | if (accumulated.length === 0) { 93 | currentRow.category = 1; 94 | accumulated.push(currentRow); 95 | return accumulated; 96 | } 97 | 98 | const tail = accumulated.pop(); 99 | // create new moment objects because moment methods are mutating 100 | const grainStart = moment(tail.date.startOf(dateGrain)); 101 | const grainEnd = moment(tail.date.endOf(dateGrain)); 102 | if (currentRow.date.isBetween(grainStart, grainEnd, 'day', '[]')) { 103 | tail.category++; 104 | tail.quantity += currentRow.quantity; 105 | tail.total += currentRow.total; 106 | accumulated.push(tail); 107 | return accumulated; 108 | } 109 | 110 | currentRow.category = 1; 111 | accumulated.push(tail, currentRow); 112 | return accumulated; 113 | }, []); 114 | } 115 | 116 | function _applyCategoryFilter(query, categoryFilter) { 117 | if (categoryFilter !== SAMPLE_APP.DEFAULT_CATEGORY) { 118 | return query.filter(transaction => { 119 | return transaction.category === categoryFilter; 120 | }); 121 | } 122 | return query; 123 | } 124 | 125 | function _applyDateRangeFilter(query, dateRangeFilter) { 126 | if (dateRangeFilter === 'year') { 127 | const lastYear = moment().subtract(1, 'years'); 128 | return query.filter(transaction => { 129 | return transaction.date.isSame(lastYear, 'year'); 130 | }); 131 | } 132 | if (dateRangeFilter === 'quarter') { 133 | const beginQuarter = moment().subtract(1, 'years').startOf('quarter'); 134 | const endQuarter = moment().subtract(1, 'years').endOf('quarter'); 135 | return query.filter(transaction => { 136 | return transaction.date.isBetween(beginQuarter, endQuarter, 'day', '[]'); 137 | }); 138 | } 139 | return query; 140 | } 141 | } 142 | 143 | devTransactionsAnalyticsFactory.$inject = ['$q', 'SAMPLE_APP', '$timeout']; 144 | 145 | ngModule.factory('devTransactionsAnalyticsFactory', devTransactionsAnalyticsFactory); 146 | }; 147 | -------------------------------------------------------------------------------- /src/common/services/transactions-analytics-factory/prod-transactions-analytics-factory.factory.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | function prodTransactionsAnalyticsFactory(SAMPLE_APP) { 3 | const Query = require('@domoinc/query'); 4 | const domo = require('ryuu.js'); 5 | // moment library for data formatting 6 | // needs to be instantiated with moment().format() 7 | const moment = require('moment'); 8 | moment().format(); 9 | 10 | // Private variables 11 | const _dataset = 'transactions'; 12 | const _grainMap = { 13 | month: 'Month', 14 | week: 'Week', 15 | quarter: 'Quarter' 16 | }; 17 | 18 | // Public API here 19 | const service = { 20 | getTotals, 21 | getTransactionsPerX 22 | }; 23 | 24 | return service; 25 | 26 | //// Functions //// 27 | /** 28 | * gets useful totals for a certain category or date range 29 | * 30 | * @param {string} categoryFilter 31 | * @param {string or object or undefined} dateRangeFilter - either 'year' for last year, 'quarter' 32 | **for same quarter last year, or undefined 33 | * @return {promise} - promise returning object of format {transactionCount: X, productsSold: X, income: X} 34 | */ 35 | function getTotals(categoryFilter, dateRangeFilter) { 36 | let query = (new Query()).select(['category', 'quantity', 'total', 'name', 'date']); 37 | query = _applyCategoryFilter(query, categoryFilter); 38 | if (typeof dateRangeFilter !== 'undefined') { 39 | query = _applyDateRangeFilter(query, dateRangeFilter); 40 | } 41 | query.groupBy('category', { total: 'sum', quantity: 'sum', name: 'count' }); 42 | return domo.get(query.query(_dataset)).then(data => { 43 | return data.reduce((accumulated, currentRow) => { 44 | // domo.get doesn't allow us to create 'virtual' rows yet, so we just reuse the rows we don't need 45 | accumulated.transactionCount += currentRow.name; 46 | accumulated.productsSold += currentRow.quantity; 47 | accumulated.income += currentRow.total; 48 | return accumulated; 49 | }, { transactionCount: 0, productsSold: 0, income: 0 }); 50 | }); 51 | } 52 | 53 | /** 54 | * gets data on transactions per dateGrain (month, week, etc...) 55 | * @param {string} categoryFilter 56 | * @param {string/undefined} optDateGrain String of either 'month', 'week', or 'quarter'. defaults to month 57 | * @param {object/string/undefined} dateRangeFilter 'year' for last year, 'quarter' for this quarter last year, or undefined 58 | * @return {array[objects]} array of format [{date: string, total: number, quantity: number, category: number}, ...] 59 | */ 60 | function getTransactionsPerX(categoryFilter, optDateGrain, optDateRange) { 61 | let query = (new Query()).select(['date', 'total', 'quantity', 'category']); 62 | const dateGrain = typeof optDateGrain !== 'undefined' ? optDateGrain : 'month'; 63 | query = _applyCategoryFilter(query, categoryFilter); 64 | if (typeof optDateRange !== 'undefined') { 65 | query = _applyDateRangeFilter(query, optDateRange); 66 | } 67 | query = _applyDateGrainFilter(query, dateGrain); 68 | return domo.get(query.query(_dataset)).then(data => { 69 | return data.map(row => { 70 | row.date = row['Calendar' + _grainMap[dateGrain]]; 71 | return row; 72 | }); 73 | }); 74 | } 75 | 76 | function _applyDateGrainFilter(query, dateGrain) { 77 | query.dateGrain('date', dateGrain, { category: 'count' }); 78 | return query; 79 | } 80 | 81 | function _applyCategoryFilter(query, categoryFilter) { 82 | if (categoryFilter !== SAMPLE_APP.DEFAULT_CATEGORY) { 83 | query.where('category').in([categoryFilter]); 84 | } 85 | return query; 86 | } 87 | 88 | function _applyDateRangeFilter(query, dateRangeFilter) { 89 | if (dateRangeFilter === 'year') { 90 | query.previousPeriod('date', 'year'); 91 | } 92 | if (dateRangeFilter === 'quarter') { 93 | // this quarter last year 94 | query.where('date').gte(moment().subtract(1, 'years').startOf('quarter').toISOString()); 95 | query.where('date').lte(moment().subtract(1, 'years').endOf('quarter').toISOString()); 96 | } 97 | return query; 98 | } 99 | } 100 | 101 | prodTransactionsAnalyticsFactory.$inject = ['SAMPLE_APP']; 102 | 103 | ngModule.factory('prodTransactionsAnalyticsFactory', prodTransactionsAnalyticsFactory); 104 | }; 105 | -------------------------------------------------------------------------------- /src/common/services/transactions-analytics-factory/transactions-analytics-factory.factory.js: -------------------------------------------------------------------------------- 1 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2 | // !!!!!! Note about datasources !!!!!!! 3 | // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 4 | // 5 | // Normally in a production app one would 6 | // gather data from a 'datasource'. This 7 | // data is gathered through the Domo client 8 | // included by adding `domo = require('ryuu.js');` 9 | // to the top of your file. 10 | // 11 | // For our purposes we have left sample code 12 | // using the Domo client to query data. 13 | // The code you use can be chosen by changing 14 | // the value of MOCK_REQUESTS in desktop/index.js 15 | // 16 | // More information on the Domo client can 17 | // be found at https://developer.domo.com 18 | /** 19 | * transactionAnalyticsFactory 20 | * @method getTotals 21 | * @method getTransactionsPerX 22 | */ 23 | module.exports = ngModule => { 24 | function transactionsAnalyticsFactory($injector, SAMPLE_APP) { 25 | if (SAMPLE_APP.MOCK_REQUESTS) { 26 | return $injector.get('devTransactionsAnalyticsFactory'); 27 | } 28 | return $injector.get('prodTransactionsAnalyticsFactory'); 29 | } 30 | 31 | transactionsAnalyticsFactory.$inject = ['$injector', 'SAMPLE_APP']; 32 | 33 | ngModule.factory('transactionsAnalyticsFactory', transactionsAnalyticsFactory); 34 | 35 | if (ON_TEST) { 36 | require('./transactions-analytics-factory.factory.spec.js')(ngModule); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/common/services/transactions-analytics-factory/transactions-analytics-factory.factory.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('factory:transactionsAnalyticsFactory', () => { 3 | let transactionsAnalyticsFactory; 4 | 5 | beforeEach(window.module(ngModule.name)); 6 | 7 | beforeEach(() => { 8 | window.module(($provide) => { 9 | $provide.factory('SAMPLE_APP', () => ({})); 10 | }); 11 | inject(_transactionsAnalyticsFactory_ => { 12 | transactionsAnalyticsFactory = _transactionsAnalyticsFactory_; 13 | }); 14 | }); 15 | 16 | it('should test properly', () => { 17 | expect(transactionsAnalyticsFactory).to.not.equal(undefined); 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/styles/typebase.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300); 2 | 3 | /* Setup */ 4 | html { 5 | /* Change default typefaces here */ 6 | font-family: 'Open Sans', 'Helvetica Neue', Arial, Helvetica, sans-serif; 7 | font-size: $baseFontSize / 16 * 100%; 8 | /* Make everything look a little nicer in webkit */ 9 | -webkit-font-smoothing: antialiased; 10 | text-size-adjust: auto 11 | } 12 | 13 | /* Copy & Lists */ 14 | p { 15 | line-height: $leading; 16 | margin-top: $leading; 17 | margin-bottom: 0; 18 | } 19 | ul, 20 | ol { 21 | margin-top: $leading; 22 | margin-bottom: $leading; 23 | li { 24 | line-height: $leading; 25 | } 26 | ul, 27 | ol { 28 | margin-top: 0; 29 | margin-bottom: 0; 30 | } 31 | } 32 | blockquote { 33 | line-height: $leading; 34 | margin-top: $leading; 35 | margin-bottom: $leading; 36 | } 37 | 38 | /* Headings */ 39 | h1, 40 | h2, 41 | h3, 42 | h4, 43 | h5, 44 | h6 { 45 | /* Change heading typefaces here */ 46 | font-family: 'Open Sans', 'Helvetica Neue', Arial, Helvetica, sans-serif; 47 | margin-top: $leading; 48 | margin-bottom: 0; 49 | line-height: $leading; 50 | } 51 | h1 { 52 | font-size: 3 * $scale * 1rem; 53 | line-height: 3 * $leading; 54 | margin-top: 2 * $leading; 55 | } 56 | h2 { 57 | font-size: 2 * $scale * 1rem; 58 | line-height: 2 * $leading; 59 | margin-top: 2 * $leading; 60 | } 61 | h3 { 62 | font-size: 1 * $scale * 1rem; 63 | } 64 | h4 { 65 | font-size: $scale / 2 * 1rem; 66 | } 67 | h5 { 68 | font-size: $scale / 3 * 1rem; 69 | } 70 | h6 { 71 | font-size: $scale / 4 * 1rem; 72 | } 73 | 74 | /* Tables */ 75 | table { 76 | margin-top: $leading; 77 | border-spacing: 0px; 78 | border-collapse: collapse; 79 | } 80 | td, 81 | th { 82 | padding: 0; 83 | line-height: $baseLineHeight * $baseFontSize - 0px; 84 | } 85 | 86 | /* Code blocks */ 87 | code { 88 | /* Forces text to constrain to the line-height. Not ideal, but works. */ 89 | vertical-align: bottom; 90 | } 91 | 92 | /* Leading paragraph text */ 93 | .lead { 94 | font-size: $scale * 1rem; 95 | } 96 | 97 | /* Hug a the block above you */ 98 | .hug { 99 | margin-top: 0; 100 | } 101 | -------------------------------------------------------------------------------- /src/common/styles/variables.css: -------------------------------------------------------------------------------- 1 | $border: 1px solid #e5e5e5; 2 | $light-bar-color: #f8f8f8; 3 | $dark-bar-color: #eee; 4 | $text-color: #555; 5 | 6 | $light: 400; 7 | $bold: 600; 8 | 9 | $paragraph: 12px; 10 | -------------------------------------------------------------------------------- /src/desktop/components/README.md: -------------------------------------------------------------------------------- 1 | - Place desktop specific components here. 2 | - These components are common accross multiple routes. -------------------------------------------------------------------------------- /src/desktop/components/loading-mask/loading-mask.component.css: -------------------------------------------------------------------------------- 1 | @import '../../../common/styles/variables.css'; 2 | 3 | $loading-mask-color: rgba(85, 85, 85, 0.2); 4 | $loading-spinner-size: 48px; 5 | /* set to match option chosen with md-progress */ 6 | 7 | .loading-container { 8 | position: relative; /* to position loading mask properly */ 9 | } 10 | 11 | loading-mask { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | height: 100%; 16 | width: 100%; 17 | z-index: 999; 18 | background-color: $loading-mask-color; 19 | visibility: hidden; 20 | opacity: 0; 21 | transition: opacity 250ms ease-in-out, visibility 0s linear 250ms; 22 | } 23 | 24 | loading-mask.visible { 25 | visibility: visible; 26 | opacity: 1; 27 | transition-delay: 0s; 28 | pointer-events: all; 29 | } 30 | 31 | .loading-square { 32 | background: white; 33 | border-radius: 3px; 34 | box-shadow: 0 0 0 4px $loading-mask-color; /* border */ 35 | padding: 20px; 36 | padding-bottom: 15px; 37 | >.loading-spinner { 38 | margin: 0 auto; 39 | margin-bottom: 10px; 40 | height: $loading-spinner-size; 41 | width: $loading-spinner-size; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/desktop/components/loading-mask/loading-mask.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | Loading... 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/desktop/components/loading-mask/loading-mask.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./loading-mask.component.css'); 3 | 4 | ngModule.component('loadingMask', { 5 | template: require('./loading-mask.component.html'), 6 | controller: loadingMaskCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | loading: '<' 10 | // Outputs should use & bindings. 11 | } 12 | }); 13 | 14 | function loadingMaskCtrl($element) { 15 | const ctrl = this; 16 | 17 | ctrl.$onInit = $onInit; 18 | ctrl.$onChanges = $onChanges; 19 | ctrl.cubes = Array(9); 20 | 21 | function $onInit() { 22 | if (ctrl.loading) { 23 | $element.addClass('visible'); 24 | } 25 | } 26 | 27 | function $onChanges(changes) { 28 | if (typeof changes.loading !== 'undefined') { 29 | if (changes.loading.currentValue) { 30 | $element.addClass('visible'); 31 | } else { 32 | $element.removeClass('visible'); 33 | } 34 | } 35 | } 36 | } 37 | 38 | // inject dependencies here 39 | loadingMaskCtrl.$inject = ['$element']; 40 | 41 | if (ON_TEST) { 42 | require('./loading-mask.component.spec.js')(ngModule); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/desktop/components/loading-mask/loading-mask.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:loadingMask', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('loadingMask', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module(ngModule.name); 14 | window.module(($provide) => { 15 | $provide.factory('$element', () => ({ addClass: () => {}, removeClass: () => {} })); 16 | }); 17 | }); 18 | 19 | beforeEach(inject(($rootScope, _$componentController_) => { 20 | scope = $rootScope.$new(); 21 | $componentController = _$componentController_; 22 | })); 23 | 24 | it('should instantiate', () => { 25 | const $ctrl = createController({}); 26 | expect($ctrl).to.not.equal(undefined); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/desktop/components/toolbar/toolbar.component.css: -------------------------------------------------------------------------------- 1 | .md-toolbar { 2 | border-bottom: 1px solid rgba(0,0,0,0.12); 3 | } 4 | 5 | .md-toolbar-tools.no-padding { 6 | padding: 0; 7 | } 8 | 9 | .toolbar-item { 10 | padding-left: 20px; 11 | } 12 | 13 | .md-toolbar-tools .toolbar-heading { 14 | font-weight: 300; 15 | font-size: 24px; 16 | } 17 | 18 | .md-toolbar-tools h3 { 19 | font-size: 24px; 20 | font-weight: 300; 21 | margin: 0; 22 | } 23 | 24 | .md-toolbar-tools h4 { 25 | font-size: 11px; 26 | text-transform: uppercase; 27 | margin: 0; 28 | } 29 | 30 | .md-toolbar-tools .toolbar-indicators { 31 | margin: auto; 32 | } 33 | -------------------------------------------------------------------------------- /src/desktop/components/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | {{ $ctrl.toolbarText }} 5 |

6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /src/desktop/components/toolbar/toolbar.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./toolbar.component.css'); 3 | 4 | ngModule.component('toolbar', { 5 | template: require('./toolbar.component.html'), 6 | controller: toolbarCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | toolbarText: '@', 10 | headingWidth: '<' 11 | // Outputs should use & bindings. 12 | }, 13 | transclude: true 14 | }); 15 | 16 | function toolbarCtrl() { 17 | const ctrl = this; 18 | 19 | ctrl.$onInit = $onInit; 20 | 21 | function $onInit() { 22 | // Called on each controller after all the controllers have been constructed and had their bindings initialized 23 | // Use this for initialization code. 24 | } 25 | } 26 | 27 | // inject dependencies here 28 | toolbarCtrl.$inject = []; 29 | 30 | if (ON_TEST) { 31 | require('./toolbar.component.spec.js')(ngModule); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/desktop/components/toolbar/toolbar.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:toolbar', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('toolbar', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(window.module(ngModule.name)); 13 | 14 | beforeEach(inject(($rootScope, _$componentController_) => { 15 | scope = $rootScope.$new(); 16 | $componentController = _$componentController_; 17 | })); 18 | 19 | it('should instantiate', () => { 20 | const $ctrl = createController({}); 21 | expect($ctrl).to.not.equal(undefined); 22 | }); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/desktop/containers/README.md: -------------------------------------------------------------------------------- 1 | - Place desktop specific containers here. 2 | - These containers are common across routes i.e. modals, drawers, etc. -------------------------------------------------------------------------------- /src/desktop/containers/tabs-container/tabs-container.component.css: -------------------------------------------------------------------------------- 1 | @import '../../../common/styles/variables.css'; 2 | 3 | .sidebar { 4 | width: 49px; 5 | border-right: $border; 6 | background-color: $light-bar-color; 7 | padding-top: 3px; 8 | } 9 | 10 | .md-sidenav-left > div { 11 | padding: 0 20px; 12 | } 13 | 14 | ._md-nav-button-text span { 15 | font-weight: $bold; 16 | } 17 | -------------------------------------------------------------------------------- /src/desktop/containers/tabs-container/tabs-container.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | {{ category }} 8 | 9 | 10 | 11 |
12 |
13 | 18 |
19 | 20 | Products Inventory 21 | Transactions 22 | 23 | 24 | 25 |
26 |
27 | -------------------------------------------------------------------------------- /src/desktop/containers/tabs-container/tabs-container.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./tabs-container.component.css'); 3 | 4 | ngModule.component('tabsContainer', { 5 | template: require('./tabs-container.component.html'), 6 | controller: tabsContainerCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | // Outputs should use & bindings. 10 | }, 11 | transclude: true 12 | }); 13 | 14 | function tabsContainerCtrl($state, 15 | $scope, 16 | productsFactory, 17 | globalFiltersFactory, 18 | SAMPLE_APP, 19 | $mdSidenav) { 20 | const ctrl = this; 21 | 22 | ctrl.$onInit = $onInit; 23 | ctrl.goToPage = goToPage; 24 | ctrl.toggleSidenav = toggleSidenav; 25 | ctrl.onCategorySelect = onCategorySelect; 26 | ctrl.selectedTab = 'products'; 27 | ctrl.categoryFilters = []; 28 | ctrl.categoryFilter = ''; 29 | 30 | productsFactory.getProductCategories().then(categories => { 31 | // add our default category to the list of categories 32 | categories.unshift(SAMPLE_APP.DEFAULT_CATEGORY); 33 | ctrl.categoryFilters = categories; 34 | ctrl.categoryFilter = ctrl.categoryFilters[0]; 35 | }); 36 | 37 | function $onInit() { 38 | // Called on each controller after all the controllers have been constructed and had their bindings initialized 39 | // Use this for initialization code. 40 | // set a watch on the current state name so we can set the tabs properly 41 | $scope.state = $state; 42 | $scope.$watch('state.current.name', newValue => { 43 | ctrl.selectedTab = newValue; 44 | }); 45 | ctrl.selectedTab = $state.current.name; 46 | } 47 | 48 | function toggleSidenav() { 49 | $mdSidenav('filters').toggle(); 50 | } 51 | 52 | function goToPage(page) { 53 | $state.go(page); 54 | } 55 | 56 | function onCategorySelect() { 57 | // tabs-container will not listen for filter changes because 58 | // it is in charge of global filters. We don't want to promote shared state 59 | // and confusing ownership. 60 | // The only reason globalFiltersFactory exists is to pass information 61 | // to children around ui-router, which does not allow for one-way databinding 62 | // to views that consist of components 63 | globalFiltersFactory.setFilter(ctrl.categoryFilter); 64 | toggleSidenav(); 65 | } 66 | } 67 | 68 | // inject dependencies here 69 | tabsContainerCtrl.$inject = ['$state', '$scope', 'productsFactory', 70 | 'globalFiltersFactory', 'SAMPLE_APP', '$mdSidenav']; 71 | 72 | if (ON_TEST) { 73 | require('./tabs-container.component.spec.js')(ngModule); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/desktop/containers/tabs-container/tabs-container.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:tabsContainer', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('tabsContainer', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(window.module(ngModule.name)); 13 | 14 | beforeEach(inject(($rootScope, _$componentController_) => { 15 | scope = $rootScope.$new(); 16 | $componentController = _$componentController_; 17 | })); 18 | 19 | it('should instantiate', () => { 20 | const $ctrl = createController({}); 21 | expect($ctrl).to.not.equal(undefined); 22 | }); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/desktop/desktop.config.js: -------------------------------------------------------------------------------- 1 | module.exports = config; 2 | 3 | function config($urlRouterProvider, $mdThemingProvider, $mdIconProvider) { 4 | $urlRouterProvider.otherwise('/'); 5 | const customPrimary = { 6 | '50': '#ffffff', // white background color for toolbar 7 | '500': '#72b0d7', // blue color of the label and choices in select 8 | '600': '#72b0d7', // blue color of the selected choice in select 9 | '700': '#80c25d', // green color of little in-stock indicator 10 | }; 11 | const customPrimaryPalette = $mdThemingProvider.extendPalette('blue', customPrimary); 12 | $mdThemingProvider.definePalette('domo-primary', customPrimaryPalette); 13 | 14 | const customAccent = { 15 | 'A200': '#72b0d7' // blue accent color for tabs 16 | }; 17 | const customAccentPalette = $mdThemingProvider.extendPalette('blue', customAccent); 18 | $mdThemingProvider.definePalette('domo-accent', customAccentPalette); 19 | 20 | const customWarn = { 21 | '600': '#fbad56', // orange pill 22 | '700': '#e4584f' // red warn of little in-stock indicator 23 | }; 24 | const customWarnPalette = $mdThemingProvider.extendPalette('red', customWarn); 25 | $mdThemingProvider.definePalette('domo-warn', customWarnPalette); 26 | 27 | const customBackground = { 28 | '50': '#fff', // white (background color) 29 | '200': '#eee', // gray background color of selected autocomplete items 30 | 'A100': '#fff' // white background color of autocomplete 31 | }; 32 | const customBackgroundPalette = $mdThemingProvider.extendPalette('grey', customBackground); 33 | $mdThemingProvider.definePalette('domo-background', customBackgroundPalette); 34 | 35 | $mdThemingProvider.theme('default') 36 | .primaryPalette('domo-primary', { 37 | 'hue-1': '50' 38 | }) 39 | .accentPalette('domo-accent') 40 | .warnPalette('domo-warn') 41 | .backgroundPalette('domo-background', { 42 | 'default': '50' 43 | }); 44 | 45 | $mdIconProvider.defaultFontSet('iconbits'); 46 | } 47 | 48 | config.$inject = ['$urlRouterProvider', '$mdThemingProvider', '$mdIconProvider']; 49 | -------------------------------------------------------------------------------- /src/desktop/desktop.css: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/angular-material/angular-material.css'; 2 | @import '../common/styles/variables.css'; 3 | 4 | h1,h2,h3,h4,h5,h6,p { 5 | color: $text-color; 6 | } 7 | 8 | #appContainer { 9 | width: 1165px; 10 | height: 830px; 11 | border-radius: 4px; 12 | font-family: "Open Sans", "Helvetica Neue", Arial, Helvetica, sans-serif; 13 | overflow: hidden; 14 | position: absolute; 15 | } 16 | 17 | .header-bar { 18 | height: 59px; 19 | background-color: #f7f7f7; 20 | border-bottom: $border; 21 | & h2 { 22 | text-align: center; 23 | padding-top: 8px; 24 | margin-top: 0; 25 | } 26 | } 27 | 28 | .iconbits { 29 | font-family: "iconbits"; 30 | } 31 | 32 | * [class^="i-"]:before, 33 | * [class*=" i-"]:before { 34 | display: inline-block; 35 | vertical-align: middle; 36 | line-height: 1; 37 | font-weight: normal; 38 | font-style: normal; 39 | text-decoration: inherit; 40 | text-transform: none; 41 | text-rendering: optimizeLegibility; 42 | -webkit-font-smoothing: antialiased; 43 | -moz-osx-font-smoothing: grayscale; 44 | } 45 | 46 | .i-chevron-down:before { 47 | content: '\e062'; 48 | } 49 | 50 | .i-chevron-up:before { 51 | content: '\e06e'; 52 | } 53 | 54 | .i-filter:before { 55 | content: '\e13e'; 56 | } 57 | 58 | .iconbits-large { 59 | font-size: 20px; 60 | } 61 | 62 | @font-face { 63 | font-family: iconbits; 64 | src: url("https://domoapps.s3.amazonaws.com/cdn/domo-bits/v1/iconbits.woff"); 65 | } 66 | -------------------------------------------------------------------------------- /src/desktop/desktop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {%= o.htmlWebpackPlugin.options.title %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 | {% if (o.htmlWebpackPlugin.options.dev) { %}{% } %} 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/desktop/desktop.init.js: -------------------------------------------------------------------------------- 1 | module.exports = init; 2 | 3 | function init() { 4 | // Add any app initialization code here. 5 | } 6 | 7 | init.$inject = []; 8 | -------------------------------------------------------------------------------- /src/desktop/index.js: -------------------------------------------------------------------------------- 1 | require('angular-material'); 2 | require('./desktop.css'); 3 | 4 | import angular from 'angular'; 5 | import { attachAll, getNgModuleNames } from '../../other/boilerplate-utils.js'; 6 | 7 | const ngDependencies = [ 8 | 'ui.router', 9 | 'ngAnimate', 10 | require('../common').name, 11 | // Add additional external Angular dependencies here 12 | 'ngMaterial' 13 | ]; 14 | 15 | ngDependencies.push.apply(ngDependencies, getNgModuleNames(require.context('./routes', true, /index\.js$/))); 16 | 17 | 18 | const ngModule = angular.module('da.desktop', ngDependencies) 19 | .constant('$', require('jquery')) 20 | .constant('d3', require('d3')) 21 | .constant('_', require('lodash')) 22 | .constant('SAMPLE_APP', { 23 | E_CAT_FILTER_CHANGE: 'filters:change', // event string for category filter change 24 | DEFAULT_CATEGORY: 'All Categories', 25 | MOCK_REQUESTS: true // swap out real DataSource requests with JSON 26 | }); 27 | 28 | attachAll(require.context('./components', true, /\.(component|directive)\.js$/))(ngModule); 29 | attachAll(require.context('./containers', true, /\.(component|directive)\.js$/))(ngModule); 30 | 31 | ngModule.config(require('./desktop.config.js')) 32 | .run(require('./desktop.init.js')); 33 | -------------------------------------------------------------------------------- /src/desktop/routes/README.md: -------------------------------------------------------------------------------- 1 | - Place routes here. 2 | - Routes allow you to group components that are only shown for a specific URL 3 | - Use `plop route` to get started. -------------------------------------------------------------------------------- /src/desktop/routes/products/components/README.md: -------------------------------------------------------------------------------- 1 | - Place route specific components here. 2 | - These components are are only used in the home route. -------------------------------------------------------------------------------- /src/desktop/routes/products/components/inventory-tools/inventory-tools.component.css: -------------------------------------------------------------------------------- 1 | .inventory-search-bar { 2 | position: relative; 3 | top: 4px; /* fix top/bottom alignment without changing container */ 4 | padding: 0 20px 0 20px; 5 | } 6 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/inventory-tools/inventory-tools.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
7 |

{{ text }}

8 |

9 | {{ $ctrl.inventoryValue !== undefined ? '$' : '' }}{{ value | summary:true:2 | default: 'Loading...' }} 10 |

11 |

12 | {{ value | summary:false:0 | default: 'Loading...' }} 13 |

14 |
15 |
16 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/inventory-tools/inventory-tools.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./inventory-tools.component.css'); 3 | 4 | ngModule.component('inventoryTools', { 5 | template: require('./inventory-tools.component.html'), 6 | controller: inventoryToolsCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | searchBarItems: '<', 10 | inventoryValue: '<', 11 | uniqueProducts: '<', 12 | totalQuantity: '<', 13 | filterFunction: '<', 14 | // Outputs should use & bindings. 15 | onSearchTextUpdate: '&' 16 | } 17 | }); 18 | 19 | function inventoryToolsCtrl() { 20 | const ctrl = this; 21 | 22 | ctrl.$onInit = $onInit; 23 | ctrl.searchTextUpdate = searchTextUpdate; 24 | 25 | function $onInit() { 26 | // Called on each controller after all the controllers have been constructed and had their bindings initialized 27 | // Use this for initialization code. 28 | } 29 | 30 | function searchTextUpdate(searchText) { 31 | ctrl.onSearchTextUpdate({ searchText }); 32 | } 33 | } 34 | 35 | // inject dependencies here 36 | inventoryToolsCtrl.$inject = []; 37 | 38 | if (ON_TEST) { 39 | require('./inventory-tools.component.spec.js')(ngModule); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/inventory-tools/inventory-tools.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:inventoryTools', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('inventoryTools', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module('ui.router'); 14 | window.module(ngModule.name); 15 | }); 16 | 17 | beforeEach(inject(($rootScope, _$componentController_) => { 18 | scope = $rootScope.$new(); 19 | $componentController = _$componentController_; 20 | })); 21 | 22 | it('should instantiate', () => { 23 | const $ctrl = createController({}); 24 | expect($ctrl).to.not.equal(undefined); 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/product-table/product-table.component.css: -------------------------------------------------------------------------------- 1 | @import '../../../../../common/styles/variables.css'; 2 | 3 | .product-table-container { 4 | overflow: scroll; 5 | } 6 | 7 | table.product-table { 8 | border-collapse: collapse; 9 | text-align: left; 10 | width: 100%; 11 | } 12 | 13 | table.product-table td, .column-headings .column-heading { 14 | padding: 20px; 15 | } 16 | 17 | .column-headings .column-heading { 18 | background-color: $light-bar-color; 19 | font-size: $paragraph; 20 | font-weight: $light; 21 | border-left: $border; 22 | border-bottom: $border; 23 | outline: none; 24 | cursor: pointer; 25 | user-select: none; 26 | text-transform: uppercase; 27 | } 28 | 29 | .column-headings > div:first-child > .column-heading:first-child { 30 | border-left: none; 31 | } 32 | 33 | .column-headings .column-heading.column-sorted { 34 | background-color: $dark-bar-color; 35 | } 36 | 37 | table.product-table tbody td { 38 | border-left: $border; 39 | font-size: $paragraph; 40 | border-bottom: $border; 41 | font-weight: $light; 42 | } 43 | 44 | table.product-table tbody td:first-child { 45 | border-left: none; 46 | } 47 | 48 | td.flex-16 { 49 | box-sizing: border-box; 50 | flex: 16.66%; 51 | } 52 | 53 | td.flex-12 { 54 | box-sizing: border-box; 55 | flex: 12.5%; 56 | } 57 | 58 | td.flex-37 { 59 | box-sizing: border-box; 60 | flex: 37.5%; 61 | } 62 | 63 | td.number { 64 | text-align: right; 65 | } 66 | 67 | .stock-indicator-circle { 68 | width: 10px; 69 | height: 10px; 70 | border-radius: 5px; 71 | margin: 0 auto; 72 | position: relative; 73 | top: 4px; 74 | } 75 | 76 | 77 | .sort-arrow { 78 | font-size: 10px; 79 | text-align: right; 80 | float: right; 81 | } 82 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/product-table/product-table.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
8 | {{ heading.title }} 9 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 24 | 27 | 30 | 33 | 36 | 37 | 38 |
20 |
21 |   22 |
23 |
25 | {{ product.name }} 26 | 28 | {{ product.price | currency }} 29 | 31 | {{ product.category }} 32 | 34 | {{ product.quantity }} 35 |
39 |
40 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/product-table/product-table.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./product-table.component.css'); 3 | 4 | ngModule.component('productTable', { 5 | template: require('./product-table.component.html'), 6 | controller: productTableCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | headings: '<', 10 | products: '<' 11 | // Outputs should use & bindings. 12 | } 13 | }); 14 | 15 | function productTableCtrl() { 16 | const ctrl = this; 17 | 18 | ctrl.orderBy = orderBy; 19 | 20 | ctrl.orderByProperty = 'inStock'; 21 | ctrl.reverseOrder = false; 22 | 23 | /** 24 | * function called on click. Checks to see if we have already sorted by this 25 | * property, if we have it will reverse the sort order. If not, it will 26 | * initiate sorting by that property 27 | * @param {string} property property to sort by (i.e. category, name, price...) 28 | */ 29 | function orderBy(property) { 30 | ctrl.reverseOrder = (ctrl.orderByProperty === property) ? !ctrl.reverseOrder : false; 31 | ctrl.orderByProperty = property; 32 | } 33 | } 34 | 35 | // inject dependencies here 36 | productTableCtrl.$inject = []; 37 | 38 | if (ON_TEST) { 39 | require('./product-table.component.spec.js')(ngModule); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/product-table/product-table.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:productTable', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('productTable', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module('ui.router'); 14 | window.module(ngModule.name); 15 | }); 16 | 17 | beforeEach(inject(($rootScope, _$componentController_) => { 18 | scope = $rootScope.$new(); 19 | $componentController = _$componentController_; 20 | })); 21 | 22 | it('should instantiate', () => { 23 | const $ctrl = createController({}); 24 | expect($ctrl).to.not.equal(undefined); 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/search-bar/search-bar.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomoApps/advanced-sample-app/a2980ae942ff19e0d14fc0026b3095ec5bc2bc4c/src/desktop/routes/products/components/search-bar/search-bar.component.css -------------------------------------------------------------------------------- /src/desktop/routes/products/components/search-bar/search-bar.component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | {{ item }} 10 | 11 | 12 | 13 | No items matching "{{ $ctrl.searchText }}" were found 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/search-bar/search-bar.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./search-bar.component.css'); 3 | 4 | ngModule.component('searchBar', { 5 | template: require('./search-bar.component.html'), 6 | controller: searchBarCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | items: '<', 10 | filterFunction: '<', 11 | // Outputs should use & bindings. 12 | onSearchTextUpdate: '&' 13 | } 14 | }); 15 | 16 | function searchBarCtrl() { 17 | const ctrl = this; 18 | 19 | ctrl.$onInit = $onInit; 20 | ctrl.searchText = ''; 21 | ctrl.onSelectedItemChange = onSelectedItemChange; 22 | ctrl.onSearchTextChange = onSearchTextChange; 23 | 24 | function $onInit() { 25 | // Called on each controller after all the controllers have been constructed and had their bindings initialized 26 | // Use this for initialization code. 27 | } 28 | 29 | /** 30 | * called when a new item is selected in the autocomplete search bar 31 | * notifies the component's parent of the search text change 32 | */ 33 | function onSelectedItemChange() { 34 | // this is called even if the user has just cleared the 35 | // search bar 36 | // check to make sure user hasn't just cleared the search bar 37 | if (typeof ctrl.selectedItem !== 'undefined') { 38 | ctrl.onSearchTextUpdate({ searchText: ctrl.searchText }); 39 | } 40 | } 41 | 42 | function onSearchTextChange() { 43 | ctrl.onSearchTextUpdate({ searchText: ctrl.searchText }); 44 | } 45 | } 46 | 47 | // inject dependencies here 48 | searchBarCtrl.$inject = []; 49 | 50 | if (ON_TEST) { 51 | require('./search-bar.component.spec.js')(ngModule); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/desktop/routes/products/components/search-bar/search-bar.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:searchBar', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('searchBar', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module('ui.router'); 14 | window.module(ngModule.name); 15 | }); 16 | 17 | beforeEach(inject(($rootScope, _$componentController_) => { 18 | scope = $rootScope.$new(); 19 | $componentController = _$componentController_; 20 | })); 21 | 22 | it('should instantiate', () => { 23 | const $ctrl = createController({}); 24 | expect($ctrl).to.not.equal(undefined); 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/desktop/routes/products/containers/inventory-container/inventory-container.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DomoApps/advanced-sample-app/a2980ae942ff19e0d14fc0026b3095ec5bc2bc4c/src/desktop/routes/products/containers/inventory-container/inventory-container.component.css -------------------------------------------------------------------------------- /src/desktop/routes/products/containers/inventory-container/inventory-container.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 12 | 13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/desktop/routes/products/containers/inventory-container/inventory-container.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./inventory-container.component.css'); 3 | 4 | ngModule.component('inventoryContainer', { 5 | template: require('./inventory-container.component.html'), 6 | controller: inventoryContainerCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | // Outputs should use & bindings. 10 | } 11 | }); 12 | 13 | function inventoryContainerCtrl( 14 | $q, 15 | productsFactory, 16 | productTableHeader, 17 | globalFiltersFactory, 18 | SAMPLE_APP 19 | ) { 20 | const ctrl = this; 21 | // private 22 | let _products = []; 23 | let _categories = []; 24 | 25 | ctrl.onSearchbarUpdate = onSearchbarUpdate; 26 | ctrl.filterByName = filterByName; 27 | 28 | ctrl.filteredCategories = []; 29 | ctrl.filteredProducts = []; 30 | ctrl.headerInfo = productTableHeader; 31 | ctrl.loading = true; 32 | ctrl.searchBarItems = []; 33 | ctrl.searchText = ''; 34 | 35 | _getToolbarItems(globalFiltersFactory.getFilter()); 36 | 37 | const productsPromise = _getProducts(globalFiltersFactory.getFilter()); 38 | const categoriesPromise = _getCategories(); 39 | $q.all([productsPromise, categoriesPromise]).then(() => { 40 | ctrl.filteredProducts = _products; 41 | _filterCategories(globalFiltersFactory.getFilter()); 42 | _buildAutocompleteList(); 43 | ctrl.loading = false; 44 | }); 45 | 46 | globalFiltersFactory.onFilterChange(_handleGlobalCategoryChange); 47 | 48 | 49 | /** 50 | * refilters the products based on search text 51 | * @param {string} searchText 52 | */ 53 | function onSearchbarUpdate(searchText) { 54 | ctrl.searchText = searchText.toLowerCase(); 55 | _filterProducts(); 56 | } 57 | 58 | function _handleGlobalCategoryChange(e, newCategory) { 59 | ctrl.loading = true; 60 | _getToolbarItems(newCategory); 61 | _filterCategories(newCategory); 62 | _getProducts(newCategory).then(() => { 63 | _filterProducts(); 64 | _buildAutocompleteList(); 65 | ctrl.loading = false; 66 | }); 67 | } 68 | 69 | function _getProducts(category) { 70 | return productsFactory.getProducts(category).then(products => { 71 | _products = products.map(product => { 72 | product.inStock = (product.quantity !== 0); 73 | return product; 74 | }); 75 | }); 76 | } 77 | 78 | function _getCategories() { 79 | return productsFactory.getProductCategories().then(categories => { 80 | _categories = categories; 81 | }); 82 | } 83 | 84 | /** 85 | * retrieves metrics for a certain category 86 | * @param {string} category category to filter by 87 | */ 88 | function _getToolbarItems(category) { 89 | ctrl.uniqueProducts = undefined; 90 | productsFactory.getNumUniqueProducts(category).then(numUniqueProducts => { 91 | ctrl.uniqueProducts = numUniqueProducts; 92 | }); 93 | ctrl.totalQuantity = undefined; 94 | productsFactory.getTotalQuantity(category).then(totalQuantity => { 95 | ctrl.totalQuantity = totalQuantity; 96 | }); 97 | ctrl.inventoryValue = undefined; 98 | productsFactory.getInventoryValue(category).then(inventoryValue => { 99 | ctrl.inventoryValue = inventoryValue; 100 | }); 101 | } 102 | 103 | function _filterCategories(newCategory) { 104 | if (newCategory !== SAMPLE_APP.DEFAULT_CATEGORY) { 105 | ctrl.filteredCategories = [newCategory]; 106 | } else { 107 | ctrl.filteredCategories = _categories; 108 | } 109 | } 110 | 111 | // will filter products by both product name and category 112 | function _filterProducts() { 113 | ctrl.filteredProducts = _products.filter(product => { 114 | // either category or name can match 115 | return ((product.name.toLowerCase().indexOf(ctrl.searchText) !== -1) 116 | || (product.category.toLowerCase().indexOf(ctrl.searchText) !== -1)); 117 | }); 118 | } 119 | 120 | function _buildAutocompleteList() { 121 | ctrl.searchBarItems = ctrl.filteredCategories.concat(ctrl.filteredProducts.map(product => { 122 | return product.name; 123 | })); 124 | } 125 | 126 | // this function is to be passed down to the search-bar 127 | // it's up here so both filtering functions are next to each other 128 | // it's separate from _filterProducts so we don't have to transform arrays 129 | // around to fit a single interface 130 | function filterByName(searchText, items) { 131 | const lowerCaseSearchText = searchText.toLowerCase(); 132 | if (lowerCaseSearchText !== '') { 133 | return items.filter(item => { 134 | return item.toLowerCase().indexOf(lowerCaseSearchText) !== -1; 135 | }); 136 | } 137 | return items; 138 | } 139 | } 140 | 141 | // inject dependencies here 142 | inventoryContainerCtrl.$inject = ['$q', 'productsFactory', 'productTableHeader', 'globalFiltersFactory', 'SAMPLE_APP']; 143 | 144 | if (ON_TEST) { 145 | require('./inventory-container.component.spec.js')(ngModule); 146 | } 147 | }; 148 | -------------------------------------------------------------------------------- /src/desktop/routes/products/containers/inventory-container/inventory-container.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:inventoryContainer', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('inventoryContainer', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module('ui.router'); 14 | window.module(ngModule.name); 15 | window.module(($provide) => { 16 | $provide.factory('productsFactory', () => ({ getNumUniqueProducts: () => new Promise(resolve => resolve()), getTotalQuantity: () => new Promise(resolve => resolve()), getInventoryValue: () => new Promise(resolve => resolve()), getProducts: () => new Promise(resolve => resolve()), getProductCategories: () => new Promise(resolve => resolve()) })); 17 | }); 18 | window.module(($provide) => { 19 | $provide.factory('globalFiltersFactory', () => ({ getFilter: () => {}, onFilterChange: () => {} })); 20 | }); 21 | window.module(($provide) => { 22 | $provide.factory('SAMPLE_APP', () => ({})); 23 | }); 24 | window.module(($provide) => { 25 | $provide.value('productTableHeader', []); 26 | }); 27 | }); 28 | 29 | beforeEach(inject(($rootScope, _$componentController_) => { 30 | scope = $rootScope.$new(); 31 | $componentController = _$componentController_; 32 | })); 33 | 34 | it('should instantiate', () => { 35 | const $ctrl = createController({}); 36 | expect($ctrl).to.not.equal(undefined); 37 | }); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/desktop/routes/products/index.js: -------------------------------------------------------------------------------- 1 | import { attachAll } from '../../../../other/boilerplate-utils.js'; 2 | 3 | const ngModule = angular.module('da.desktop.products', []); 4 | 5 | attachAll(require.context('./components', true, /\.(component|directive)\.js$/))(ngModule); 6 | attachAll(require.context('./containers', true, /\.(component|directive)\.js$/))(ngModule); 7 | 8 | ngModule.config(productsConfig); 9 | 10 | 11 | function productsConfig($stateProvider) { 12 | $stateProvider.state('products', { 13 | url: '/', 14 | template: '' 15 | }); 16 | } 17 | 18 | productsConfig.$inject = ['$stateProvider']; 19 | 20 | 21 | export default ngModule; 22 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/README.md: -------------------------------------------------------------------------------- 1 | - Place route specific components here. 2 | - These components are are only used in the transactions route. -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/line-chart/line-chart.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | const d3 = require('d3'); 3 | require('@domoinc/multi-line-chart'); 4 | 5 | ngModule.component('lineChart', { 6 | template: '', 7 | controller: lineChartCtrl, 8 | bindings: { 9 | // Inputs should use < and @ bindings. 10 | chartData: '<', 11 | lineColor: '<' 12 | // Outputs should use & bindings. 13 | } 14 | }); 15 | 16 | function lineChartCtrl($element, $timeout) { 17 | const ctrl = this; 18 | let _chart = undefined; 19 | const _svgHeight = 650; 20 | const _svgWidth = 528; 21 | const _lineChartHeight = 620; 22 | const _lineChartWidth = 478; 23 | // the chart's axes are by default larger than the svg element 24 | const _translateX = 25; 25 | const _translateY = 5; 26 | 27 | ctrl.$onInit = $onInit; 28 | ctrl.$postLink = $postLink; 29 | ctrl.$onChanges = $onChanges; 30 | ctrl.$onDestroy = $onDestroy; 31 | 32 | function $onInit() { 33 | // Called on each controller after all the controllers have been constructed and had their bindings initialized 34 | // Use this for initialization code. 35 | } 36 | 37 | function $postLink() { 38 | _chart = d3.select($element.children()[0]) 39 | .attr('height', _svgHeight) 40 | .attr('width', _svgWidth) 41 | .append('g') 42 | // offset the chart so we can see the axes 43 | .attr('transform', 'translate(' + _translateX + ',' + _translateY + ')') 44 | .chart('MultiLineChart') 45 | .c({ 46 | height: _lineChartHeight, 47 | width: _lineChartWidth, 48 | strokeWidth: 3, 49 | xAddAxis: { name: 'Show', value: true }, 50 | xAddGridlines: { name: 'Show', value: true }, 51 | yAddZeroline: { name: 'Hide', value: false }, 52 | singleColor: ctrl.lineColor 53 | }).a('X Axis', line => { 54 | // override default accessor, it doesn't accept "quarter" values (ex "2015 Q1") 55 | return line[0]; 56 | }); 57 | /** 58 | * wrap chart drawing in a timeout 59 | * 60 | * for some reason the DOM is not painted when $postLink is run, which messes with d3 61 | * 62 | * by running a timeout(fn, 0) we add our _chart.draw to the end of the event queue, 63 | * after the first layout paint 64 | */ 65 | $timeout(() => { 66 | _chart.draw(ctrl.chartData); 67 | }, 0, false); 68 | } 69 | 70 | function $onChanges(changes) { 71 | if (typeof changes.chartData !== 'undefined' || typeof changes.lineColor !== 'undefined') { 72 | if (typeof changes.lineColor !== 'undefined') { 73 | _chart.c({ singleColor: ctrl.lineColor }); 74 | } 75 | _chart.draw(ctrl.chartData); 76 | } 77 | } 78 | 79 | function $onDestroy() { 80 | _chart = d3.select($element.children()[0]).remove(); 81 | } 82 | } 83 | 84 | // inject dependencies here 85 | lineChartCtrl.$inject = ['$element', '$timeout']; 86 | 87 | if (ON_TEST) { 88 | require('./line-chart.component.spec.js')(ngModule); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/line-chart/line-chart.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:lineChart', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('lineChart', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module('ui.router'); 14 | window.module(ngModule.name); 15 | window.module(($provide) => { 16 | $provide.factory('$element', () => ({})); 17 | }); 18 | }); 19 | 20 | beforeEach(inject(($rootScope, _$componentController_) => { 21 | scope = $rootScope.$new(); 22 | $componentController = _$componentController_; 23 | })); 24 | 25 | it('should instantiate', () => { 26 | const $ctrl = createController({}); 27 | expect($ctrl).to.not.equal(undefined); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/pill/pill.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | const d3 = require('d3'); 3 | require('@domoinc/ca-icon-trends-with-text'); 4 | 5 | ngModule.component('pill', { 6 | template: '', 7 | controller: pillCtrl, 8 | bindings: { 9 | // Inputs should use < and @ bindings. 10 | chartData: '<', 11 | pillTitle: '<', 12 | pillCaption: '<', 13 | pillColor: '<', 14 | isActive: '<', 15 | // Outputs should use & bindings. 16 | onClick: '&' 17 | } 18 | }); 19 | 20 | function pillCtrl($element, $timeout) { 21 | const ctrl = this; 22 | let _pill = undefined; 23 | let _circle = undefined; 24 | const _grayColor = '#bbb'; 25 | 26 | const _centerOffset = '.35em'; // y offset to center text in pill 27 | const _textSizes = { 28 | small: '12', 29 | large: '23' 30 | }; 31 | const _pillWidth = 480; 32 | const _pillHeight = 121; 33 | 34 | ctrl.$onInit = $onInit; 35 | ctrl.$postLink = $postLink; 36 | ctrl.$onChanges = $onChanges; 37 | ctrl.$onDestroy = $onDestroy; 38 | ctrl.pillClicked = pillClicked; 39 | 40 | function $onInit() { 41 | // Called on each controller after all the controllers have been constructed and had their bindings initialized 42 | // Use this for initialization code. 43 | } 44 | 45 | function $postLink() { 46 | const chartOptions = { 47 | width: _pillWidth, 48 | height: _pillHeight 49 | }; 50 | // merge color options object with chartOptions 51 | Object.assign(chartOptions, _getColors()); 52 | _pill = d3.select($element.children()[0]).insert('g') 53 | .chart('CAIconTrendsWithText') 54 | .c(chartOptions); 55 | 56 | /** 57 | * wrap chart drawing in a timeout 58 | * 59 | * for some reason the DOM is not painted when $postLink is run, which messes with 60 | * `getBBox()` and d3 in general 61 | * 62 | * by running a timeout(fn, 0) we add our _chart.draw to the end of the event queue, 63 | * after the first layout paint 64 | */ 65 | $timeout(() => { 66 | _pill.draw(ctrl.chartData); 67 | _circle = d3.select($element.children()[0]).select(' .iconCircle').node(); 68 | 69 | // create a parent of _circle 70 | d3.select(_circle.parentNode) 71 | .insert('g', () => { return _circle; }) 72 | .append(() => { return _circle; }); 73 | 74 | const circleBBox = _circle.getBBox(); 75 | const xloc = circleBBox.x + (circleBBox.width / 2); 76 | const yloc = circleBBox.y + (circleBBox.height / 2); 77 | ['small', 'large'].forEach(textType => { 78 | // add text inside the parent and size them 79 | // to fit inside the circle 80 | const fontSize = (textType === 'small' ? _textSizes.small : _textSizes.large); 81 | const alignemtnBaseline = (textType === 'small' ? 'hanging' : 'alphabetic'); 82 | d3.select(_circle.parentNode) 83 | .append('text') 84 | .attr('class', 'text-' + textType) 85 | .attr('transform', 'translate(' + xloc + ',' + (textType === 'small' ? yloc : yloc - 10) + ')') // align large text a little higher 86 | .attr('text-anchor', 'middle') 87 | .attr('alignment-baseline', alignemtnBaseline) 88 | .attr('dy', _centerOffset) 89 | .attr('font-size', fontSize); 90 | }); 91 | _changeText(_circle, ctrl.pillTitle, ctrl.pillCaption); 92 | }, 0, false); 93 | } 94 | 95 | function $onChanges(changes) { 96 | if (typeof changes.chartData !== 'undefined') { 97 | _pill.draw(changes.chartData.currentValue); 98 | } 99 | if (typeof changes.pillTitle !== 'undefined') { 100 | _changeText(_circle, changes.pillTitle.currentValue, ctrl.pillCaption); 101 | } 102 | if (typeof changes.pillCaption !== 'undefined') { 103 | _changeText(_circle, ctrl.pillTitle, changes.pillCaption.currentValue); 104 | } 105 | if (typeof changes.isActive !== 'undefined') { 106 | _pill.c(_getColors()); 107 | _pill.draw(ctrl.chartData); 108 | } 109 | } 110 | 111 | function $onDestroy() { 112 | // free up memory 113 | _pill = d3.select($element.children()[0]).remove(); 114 | } 115 | 116 | function pillClicked() { 117 | ctrl.onClick(); 118 | } 119 | 120 | function _changeText(circle, pillTitle, pillCaption) { 121 | d3.select(circle.parentNode).select('.text-large').text(pillTitle); 122 | d3.select(circle.parentNode).select('.text-small').text(pillCaption); 123 | } 124 | 125 | function _getColors() { 126 | if (ctrl.isActive) { 127 | return { 128 | generalFillBadColor: ctrl.pillColor, 129 | generalFillGoodColor: ctrl.pillColor, 130 | generalFillNeutralColor: ctrl.pillColor, 131 | generalStrokeBadColor: ctrl.pillColor, 132 | generalStrokeGoodColor: ctrl.pillColor, 133 | generalStrokeNeutralColor: ctrl.pillColor 134 | }; 135 | } 136 | return { 137 | generalFillBadColor: _grayColor, 138 | generalFillGoodColor: _grayColor, 139 | generalFillNeutralColor: _grayColor, 140 | generalStrokeBadColor: _grayColor, 141 | generalStrokeGoodColor: _grayColor, 142 | generalStrokeNeutralColor: _grayColor 143 | }; 144 | } 145 | } 146 | 147 | // inject dependencies here 148 | pillCtrl.$inject = ['$element', '$timeout']; 149 | 150 | if (ON_TEST) { 151 | require('./pill.component.spec.js')(ngModule); 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/pill/pill.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:pill', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('pill', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module('ui.router'); 14 | window.module(ngModule.name); 15 | window.module(($provide) => { 16 | $provide.factory('$element', () => ({})); 17 | }); 18 | }); 19 | 20 | beforeEach(inject(($rootScope, _$componentController_) => { 21 | scope = $rootScope.$new(); 22 | $componentController = _$componentController_; 23 | })); 24 | 25 | it('should instantiate', () => { 26 | const $ctrl = createController({}); 27 | expect($ctrl).to.not.equal(undefined); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/transaction-tools/transaction-tools.component.css: -------------------------------------------------------------------------------- 1 | $toolbar-font-size: 16px; 2 | 3 | .toolbar-selects { 4 | font-size: $toolbar-font-size; 5 | } 6 | 7 | .toolbar-selects > div { 8 | /* wrapper div around md-input-container is to help with label alignment */ 9 | position: relative; 10 | top: 5px; /* fix vertical alignment in toolbar */ 11 | } 12 | 13 | .transaction-tools-select > md-select-menu, 14 | .transaction-tools-select > md-select-value { 15 | text-transform: capitalize; 16 | } 17 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/transaction-tools/transaction-tools.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | {{ dateGrain }} 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | {{ dateRange.name }} 16 | 17 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/transaction-tools/transaction-tools.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./transaction-tools.component.css'); 3 | 4 | ngModule.component('transactionTools', { 5 | template: require('./transaction-tools.component.html'), 6 | controller: transactionToolsCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | dateRangeOptions: '<', 10 | granularityOptions: '<', 11 | selectedDateRange: '<', 12 | selectedGranularity: '<', 13 | // Outputs should use & bindings. 14 | onGranularityDropdownSelect: '&', 15 | onDateRangeDropdownSelect: '&', 16 | } 17 | }); 18 | 19 | function transactionToolsCtrl() { 20 | const ctrl = this; 21 | 22 | ctrl.granularityDropdownSelect = granularityDropdownSelect; 23 | ctrl.dateRangeDropdownSelect = dateRangeDropdownSelect; 24 | 25 | 26 | function granularityDropdownSelect() { 27 | ctrl.onGranularityDropdownSelect({ value: ctrl.selectedGranularity }); 28 | } 29 | 30 | function dateRangeDropdownSelect() { 31 | ctrl.onDateRangeDropdownSelect({ value: ctrl.selectedDateRange }); 32 | } 33 | } 34 | 35 | // inject dependencies here 36 | transactionToolsCtrl.$inject = []; 37 | 38 | if (ON_TEST) { 39 | require('./transaction-tools.component.spec.js')(ngModule); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/components/transaction-tools/transaction-tools.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:transactionTools', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = { 7 | onGranularityDropdownSelect: () => {}, 8 | onDateRangeDropdownSelect: () => {}, 9 | }) { 10 | const $ctrl = $componentController('transactionTools', { $scope: scope }, bindings); 11 | $ctrl.onGranularityDropdownSelect = () => {}; 12 | $ctrl.onDateRangeDropdownSelect = () => {}; 13 | if ($ctrl.$onInit) $ctrl.$onInit(); 14 | return $ctrl; 15 | } 16 | 17 | beforeEach(() => { 18 | window.module('ui.router'); 19 | window.module(ngModule.name); 20 | }); 21 | 22 | beforeEach(inject(($rootScope, _$componentController_) => { 23 | scope = $rootScope.$new(); 24 | $componentController = _$componentController_; 25 | })); 26 | 27 | it('should instantiate', () => { 28 | const $ctrl = createController({}); 29 | expect($ctrl).to.not.equal(undefined); 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/containers/pills-container/pills-container.component.css: -------------------------------------------------------------------------------- 1 | div.pills { 2 | svg { 3 | opacity: .4; 4 | cursor: pointer; 5 | transition: opacity .25s ease-in-out; 6 | width: 100%; 7 | } 8 | .active svg { 9 | opacity: 1; 10 | } 11 | svg:hover { 12 | opacity: 1; 13 | } 14 | pill:focus { 15 | outline: none; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/containers/pills-container/pills-container.component.html: -------------------------------------------------------------------------------- 1 |
2 | 13 | 14 |
15 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/containers/pills-container/pills-container.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./pills-container.component.css'); 3 | 4 | ngModule.component('pillsContainer', { 5 | template: require('./pills-container.component.html'), 6 | controller: pillsContainerCtrl, 7 | bindings: { 8 | // Inputs should use < and @ bindings. 9 | pillData: '<', 10 | // Outputs should use & bindings. 11 | onPillClick: '&' 12 | } 13 | }); 14 | 15 | function pillsContainerCtrl() { 16 | const ctrl = this; 17 | 18 | ctrl.$onInit = $onInit; 19 | ctrl.switchPills = switchPills; 20 | ctrl.activePill = 0; 21 | 22 | function $onInit() { 23 | // Called on each controller after all the controllers have been constructed and had their bindings initialized 24 | // Use this for initialization code. 25 | } 26 | 27 | function switchPills(pill) { 28 | ctrl.activePill = pill; 29 | ctrl.onPillClick({ pill }); 30 | } 31 | } 32 | 33 | // inject dependencies here 34 | pillsContainerCtrl.$inject = []; 35 | 36 | if (ON_TEST) { 37 | require('./pills-container.component.spec.js')(ngModule); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/containers/pills-container/pills-container.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:pillsContainer', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('pillsContainer', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module('ui.router'); 14 | window.module(ngModule.name); 15 | }); 16 | 17 | beforeEach(inject(($rootScope, _$componentController_) => { 18 | scope = $rootScope.$new(); 19 | $componentController = _$componentController_; 20 | })); 21 | 22 | it('should instantiate', () => { 23 | const $ctrl = createController({}); 24 | expect($ctrl).to.not.equal(undefined); 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/containers/transactions-container/transactions-container.component.css: -------------------------------------------------------------------------------- 1 | .transactions-container { 2 | padding: 50px 30px; 3 | } 4 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/containers/transactions-container/transactions-container.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 13 | 14 |
15 | 20 | 21 | 27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/containers/transactions-container/transactions-container.component.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | require('./transactions-container.component.css'); 3 | require('@domoinc/ca-icon-trends-with-text'); 4 | 5 | ngModule.component('transactionsContainer', { 6 | template: require('./transactions-container.component.html'), 7 | controller: transactionsContainerCtrl, 8 | }); 9 | 10 | function transactionsContainerCtrl( 11 | transactionsAnalyticsFactory, 12 | $q, 13 | globalFiltersFactory, 14 | dateRangeItems, 15 | granularityItems, 16 | transactionPillDataFactory, 17 | summaryFilter 18 | ) { 19 | const ctrl = this; 20 | 21 | let _categoryFilter = globalFiltersFactory.getFilter(); 22 | 23 | ctrl.$onInit = $onInit; 24 | 25 | ctrl.displayLineChart = displayLineChart; 26 | ctrl.granularityDropdownSelect = granularityDropdownSelect; 27 | ctrl.dateRangeDropdownSelect = dateRangeDropdownSelect; 28 | 29 | ctrl.loading = true; 30 | 31 | ctrl.pillData = transactionPillDataFactory.getPillData(); 32 | ctrl.activePillData = ctrl.pillData[0]; 33 | 34 | ctrl.dateRangeOptions = dateRangeItems; 35 | ctrl.dateRangeDropdownSelectedOption = ctrl.dateRangeOptions[0]; 36 | 37 | ctrl.granularityOptions = granularityItems; 38 | ctrl.granularityDropdownSelectedOption = ctrl.granularityOptions[0]; 39 | 40 | globalFiltersFactory.onFilterChange(_onCategoryChange); 41 | 42 | function $onInit() { 43 | _refreshData(); 44 | } 45 | 46 | function _onCategoryChange(e, newCategory) { 47 | _categoryFilter = newCategory; 48 | _refreshData(); 49 | } 50 | 51 | function _refreshData() { 52 | ctrl.loading = true; 53 | const dateRangeFilter = (typeof ctrl.dateRangeDropdownSelectedOption !== 'undefined' ? 54 | ctrl.dateRangeDropdownSelectedOption.value : undefined); 55 | const totalsPromise = transactionsAnalyticsFactory.getTotals(_categoryFilter, dateRangeFilter); 56 | const chartDataPromise = transactionsAnalyticsFactory 57 | .getTransactionsPerX(_categoryFilter, ctrl.granularityDropdownSelectedOption, dateRangeFilter); 58 | return $q.all([totalsPromise, chartDataPromise]).then(data => { 59 | ctrl.pillData[0].chart = _formatDataForLineChart('Income', data[1], 'total'); 60 | ctrl.pillData[1].chart = _formatDataForLineChart('Products Sold', data[1], 'quantity'); 61 | ctrl.pillData[2].chart = _formatDataForLineChart('Transactions', data[1], 'category'); 62 | 63 | ctrl.pillData[0].summary = '$' + summaryFilter(data[0].income, true, 1); 64 | ctrl.pillData[1].summary = summaryFilter(data[0].productsSold, false, 1); 65 | ctrl.pillData[2].summary = summaryFilter(data[0].transactionCount, false, 1); 66 | 67 | ctrl.loading = false; 68 | }); 69 | } 70 | 71 | function granularityDropdownSelect(selectedGranularity) { 72 | if (ctrl.granularityDropdownSelectedOption !== selectedGranularity) { 73 | ctrl.granularityDropdownSelectedOption = selectedGranularity; 74 | _refreshData(); 75 | } 76 | } 77 | 78 | function dateRangeDropdownSelect(selectedDateRange) { 79 | if (ctrl.dateRangeDropdownSelectedOption !== selectedDateRange) { 80 | ctrl.dateRangeDropdownSelectedOption = selectedDateRange; 81 | _refreshData(); 82 | } 83 | } 84 | 85 | function displayLineChart(chartId) { 86 | ctrl.activePillData = ctrl.pillData[chartId]; 87 | } 88 | 89 | 90 | function _formatDataForLineChart(title, salesData, columnName) { 91 | return salesData.map(row => { 92 | return [row.date, row[columnName], title]; 93 | }); 94 | } 95 | } 96 | 97 | // inject dependencies here 98 | transactionsContainerCtrl.$inject = ['transactionsAnalyticsFactory', '$q', 'globalFiltersFactory', 'dateRangeItems', 'granularityItems', 'transactionPillDataFactory', 'summaryFilter']; 99 | 100 | if (ON_TEST) { 101 | require('./transactions-container.component.spec.js')(ngModule); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/containers/transactions-container/transactions-container.component.spec.js: -------------------------------------------------------------------------------- 1 | module.exports = ngModule => { 2 | describe('component:transactionsContainer', () => { 3 | let scope; 4 | let $componentController; 5 | 6 | function createController(bindings = {}) { 7 | const $ctrl = $componentController('transactionsContainer', { $scope: scope }, bindings); 8 | if ($ctrl.$onInit) $ctrl.$onInit(); 9 | return $ctrl; 10 | } 11 | 12 | beforeEach(() => { 13 | window.module('ui.router'); 14 | window.module(ngModule.name); 15 | window.module(($provide) => { 16 | $provide.factory('globalFiltersFactory', () => ({ getFilter: () => {}, onFilterChange: () => {} })); 17 | }); 18 | window.module(($provide) => { 19 | $provide.factory('transactionsAnalyticsFactory', () => { 20 | function getTotals() { 21 | return { 22 | income: 0, 23 | productsSold: 0, 24 | transactions: 0 25 | }; 26 | } 27 | function getTransactionsPerX() { 28 | return []; 29 | } 30 | 31 | return { 32 | getTotals, 33 | getTransactionsPerX 34 | }; 35 | }); 36 | }); 37 | window.module(($provide) => { 38 | $provide.factory('$mdColors', () => ({ getThemeColor: () => {} })); 39 | }); 40 | window.module(($provide) => { 41 | $provide.factory('summaryFilter', () => ({})); 42 | }); 43 | window.module(($provide) => { 44 | $provide.factory('transactionPillDataFactory', () => ({ getPillData: () => [] })); 45 | }); 46 | window.module(($provide) => { 47 | $provide.value('dateRangeItems', []); 48 | }); 49 | window.module(($provide) => { 50 | $provide.value('granularityItems', []); 51 | }); 52 | }); 53 | 54 | beforeEach(inject(($rootScope, _$componentController_) => { 55 | scope = $rootScope.$new(); 56 | $componentController = _$componentController_; 57 | })); 58 | 59 | it('should instantiate', () => { 60 | const $ctrl = createController({}); 61 | expect($ctrl).to.not.equal(undefined); 62 | }); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /src/desktop/routes/transactions/index.js: -------------------------------------------------------------------------------- 1 | import { attachAll } from '../../../../other/boilerplate-utils.js'; 2 | 3 | const ngModule = angular.module('da.desktop.transactions', []); 4 | 5 | attachAll(require.context('./components', true, /\.(component|directive)\.js$/))(ngModule); 6 | attachAll(require.context('./containers', true, /\.(component|directive)\.js$/))(ngModule); 7 | 8 | ngModule.config(transactionsConfig); 9 | 10 | 11 | function transactionsConfig($stateProvider) { 12 | $stateProvider.state('transactions', { 13 | url: '/transactions', 14 | template: '' 15 | }); 16 | } 17 | 18 | transactionsConfig.$inject = ['$stateProvider']; 19 | 20 | 21 | export default ngModule; 22 | -------------------------------------------------------------------------------- /src/responsive/components/README.md: -------------------------------------------------------------------------------- 1 | - Place responsive specific components here. 2 | - These components are common accross multiple routes. 3 | -------------------------------------------------------------------------------- /src/responsive/containers/README.md: -------------------------------------------------------------------------------- 1 | - Place responsive specific containers here. 2 | - These containers are common across routes i.e. modals, drawers, etc. 3 | -------------------------------------------------------------------------------- /src/responsive/index.js: -------------------------------------------------------------------------------- 1 | require('./responsive.css'); 2 | 3 | import angular from 'angular'; 4 | import { attachAll, getNgModuleNames } from '../../other/boilerplate-utils.js'; 5 | 6 | const ngDependencies = [ 7 | 'ui.router', 8 | 'ngAnimate', 9 | require('../common').name, 10 | // Add additional external Angular dependencies here 11 | ]; 12 | 13 | ngDependencies.push.apply(ngDependencies, getNgModuleNames(require.context('./routes', true, /index\.js$/))); 14 | 15 | 16 | const ngModule = angular.module('da.responsive', ngDependencies) 17 | .constant('$', require('jquery')) 18 | .constant('d3', require('d3')) 19 | .constant('_', require('lodash')); 20 | 21 | attachAll(require.context('./components', true, /\.(component|directive)\.js$/))(ngModule); 22 | attachAll(require.context('./containers', true, /\.(component|directive)\.js$/))(ngModule); 23 | 24 | ngModule.config(require('./responsive.config.js')) 25 | .run(require('./responsive.init.js')); 26 | -------------------------------------------------------------------------------- /src/responsive/responsive.config.js: -------------------------------------------------------------------------------- 1 | module.exports = config; 2 | 3 | function config($urlRouterProvider) { 4 | $urlRouterProvider.otherwise('/'); 5 | } 6 | 7 | config.$inject = ['$urlRouterProvider']; 8 | -------------------------------------------------------------------------------- /src/responsive/responsive.css: -------------------------------------------------------------------------------- 1 | @import '../common/styles/variables.css'; 2 | 3 | body { 4 | color: $domo-blue; 5 | } 6 | -------------------------------------------------------------------------------- /src/responsive/responsive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {%= o.htmlWebpackPlugin.options.title %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

Responsive

22 |
23 |
24 | {% if (o.htmlWebpackPlugin.options.dev) { %}{% } %} 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/responsive/responsive.init.js: -------------------------------------------------------------------------------- 1 | module.exports = init; 2 | 3 | function init() { 4 | // Add any app initialization code here. 5 | } 6 | 7 | init.$inject = []; 8 | -------------------------------------------------------------------------------- /src/responsive/routes/README.md: -------------------------------------------------------------------------------- 1 | - Place routes here. 2 | - Routes allow you to group components that are only shown for a specific URL 3 | - Use `plop route` to get started. -------------------------------------------------------------------------------- /src/switcher.js: -------------------------------------------------------------------------------- 1 | const DESKTOP_VIEW_ON_TABLET = true; 2 | 3 | /** 4 | * DO NOT EDIT BELOW ME! 5 | */ 6 | 7 | const enquire = require('enquire.js'); 8 | 9 | 10 | function redirect() { 11 | /* Desktops and laptops ----------- */ 12 | enquire.register('only screen and (min-width : 1025px)', () => { 13 | window.location.replace('/desktop/index.html'); 14 | }); 15 | 16 | enquire.register('only screen and (max-width : 736px)', () => { 17 | window.location.replace('/responsive/index.html'); 18 | }); 19 | 20 | /* iPads (portrait and landscape) ----------- */ 21 | enquire.register('only screen and (min-width : 736px) and (orientation: portrait) and (max-width : 1024px), only screen and (min-width: 737px) and (max-width : 1024px)', () => { 22 | if (DESKTOP_VIEW_ON_TABLET) { 23 | window.location.replace('/desktop/index.html'); 24 | } else { 25 | window.location.replace('/responsive/index.html'); 26 | } 27 | }); 28 | } 29 | 30 | window.requestAnimationFrame(redirect); 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Include dependencies 2 | require('babel-register'); 3 | const getConfig = require('./other/webpack.config.es6'); 4 | 5 | /** 6 | * Configure your webpack setup here. These settings can be changed at any time. 7 | * 8 | * Read more on the wiki: 9 | * https://git.empdev.domo.com/AppTeam6/da-webpack/wiki/Webpack-Configuration 10 | */ 11 | module.exports = getConfig({ 12 | includeDesktopView: true, 13 | includeResponsiveView: false, 14 | externals: { 15 | // Include your app's extra externals here 16 | }, 17 | loaders: [ 18 | // Include your app's extra loaders here 19 | { 20 | test: /\.json$/, 21 | loader: 'json' 22 | } 23 | ] 24 | }); 25 | --------------------------------------------------------------------------------