├── .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 | [](http://commitizen.github.io/cz-cli/)
4 |
5 | 
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 |
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 |
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 |
20 |
21 |
22 |
23 | |
24 |
25 | {{ product.name }}
26 | |
27 |
28 | {{ product.price | currency }}
29 | |
30 |
31 | {{ product.category }}
32 | |
33 |
34 | {{ product.quantity }}
35 | |
36 |
37 |
38 |
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: '