Skip to content

Latest commit

 

History

History
427 lines (319 loc) · 18.3 KB

DEVELOPER.md

File metadata and controls

427 lines (319 loc) · 18.3 KB

Impac Developer Toolkit

Developer Account & Authentication

Steps to get an Impac! Workspace loaded:
  1. Fork impac-angular (https://github.com/maestrano/impac-angular) and clone it to your local machine. Note this method for running impac-angular is only valid from versions >= 1.4.5.
  2. Run bower install, npm install to ensure you have all the required dependencies.
  3. Run gulp serve or just gulp.
  4. A new tab with the Developer Workspace served will open in your browser, just create an account or log in!
  5. Check your emails, and confirm your account.
  6. You should be able to start coding in the project impac-angular: just saving any .coffee or .html file will automatically reload your Developer Workspace, applying your code.
  7. To populate the Widgets and KPIs with data, integrate an App! We recommend generating a demo account with Xero.
Workspace Architecture

The workspace environment works by loading a parent angular module, which depends on impac-angular ( 'impacWorkspace' ). This allows us to configure impac-angular's services as required.

For more information on the configurations available on impac-angular's provider services see README.md Impac Angular Providers & Services section.

angular.module('impacWorkspace', ['maestrano.impac']);

The /workspace directory serves as a mini angular app, that each file is loaded into workspace/index.html via <script> tags, followed by the /dist/impac-angular.js file.. along with all other bower and npm dependencies injected via gulp 'wiredep'.

Then the /workspace/index.html is served to the browser and the /workspace and /src directory are watched for changes.

Impac Ecosystem Architecture

The high-level diagram below outlines the ecosystem which powers Impac! Angular's reporting abilities.

Impac Ecosystem Overview

First Load Flow
  1. The hosting front-end retrieves data for User and Organizations which are then provided to Impac! Angular via an Angular Provider Service.
  2. Impac! Angular retrieves the Dashboards, Widgets & KPIs configurations from Hub API.​
  3. Impac! Angular queries Impac! Engine to retrieve data.
  4. Impac! Engine authenticates the user using IDM API.
  5. Impac! Engine queries Connec! to retrieve raw data based on the widget configuration.
  6. Connec! authenticates the user using IDM API and returns raw data to Impac! Engine.
  7. Impac! Engine computes this raw data into a meaningful summary and sends it back to Impac! Angular.
  8. Impac! Angular display the summary using a pre-defined template (line chart, pie chart, figure, list...)

How-to: Create a widget


Shortcut!

We have built a Yoeman generator to generate the boilerplate and some extras to help get you going!

  1. Run npm update to make sure you have all the latest npm packages.
  2. Simply run, yo widget, and follow the prompts to generate your new Widget Component!

** Please read the Full Process below, as it will provide more details on getting your widget up and running & help with understanding the basics.**

You want to make the generator better? Of course. See the README and take a look at generators/generator-widget.

Full Process
  1. Defining the Widgets Template.
    Widgets templates are currently kept in the maestrano api. They declare defining attributes for each widget.
    It is important to take note of the path vs path & metadata.template attributes. Defining a metadata.template enables you to use an existing Impac API engine, and points the front-end to a different template
// Example of a widget template
// -----
{
  // engine called in Impac! API
  path: 'accounts/balance',

  // optional - name of the template to use for your widget. In this case, 'accounts-balance.tmpl.html' will be used. 
  // If no metadata['template'] is defined, the path is used to determine the template name.
  metadata: {
    template: 'accounts/balance'
  },

  // name to be displayed in the widgets selector
  name: 'Account balance', 
  
  // description tooltip to be displayed in the widgets selector
  desc: "Display the current value of a given account",
  
  // font awesome icon to be displayed in the widgets selector
  icon: "pie-chart",
  
  // number of bootstrap columns of the widget (value can be 3 or 6)
  width: 3
}

Widgets templates can be stubbed in the workspace/index.js file, via the ImpacDeveloper service.

  module.factory('settings', function () {
    return {
    
      ...
      
      widgetsTemplates: [
            {
              path: 'invoices/awesome-existing-engine',
              name: 'Awesome Sales Widget',
              metadata: { template: 'sales/your-awesome-component' },
              desc: 'compares awesome things to more awesome things',
              icon: 'pie-chart',
              width: 3
            },
            {
              path: 'accounting/your-engine-and-component-name',
              name: 'Awesome Accounting Widget',
              desc: 'compares awesome things to more awesome things',
              icon: 'pie-chart',
              width: 3
            }
      ]
    }
  });

This will inject your stubbed templates into the angular apps model, displaying available templates from API and your stubbed templates.

  1. Create the widget's files:
  • in /src/components/widgets/, add a folder category-widget-name (e.g: accounts-my-new-widget).
  • in this new folder, add three files:
    • accounts-my-new-widget.directive.coffee containing the angular directive and controller defining your widget's behaviour.
    • accounts-my-new-widget.tmpl.html containing the template of your widget.
    • accounts-my-new-widget.less containing the stylesheet of your widget.
    • accounts-my-new-widget.spec.js containing unit-tests for your widget.
  1. Building the directive:

Widget directives get loaded through widget.directive.coffee's template by ngInclude, which means it inherits scope.

Below are some key variables and methods available through the ImpacWidget scope:

  • $scope.parentDashboard, which is the dashboard object that contains the widget object in its widgets list.
  • $scope.widget, which is the widget object.
  • $scope.widgetDeferred a $q promise object, see step 5.
  • $scope.updateSettings(), updates all widget-settings directives registered on the widget.

The examples below are the basic widget component set-up that is pretty much generic across all other widgets. Make sure you stick to this convention.

# Basic components directive structure
module = angular.module('impac.components.widgets.your-widget',[])

module.controller('YourWidgetCtrl', ($scope) ->

  w = $scope.widget
  
)
module.directive('yourWidget', ->
  return {
     # avoid restricting by element ('E') please.
     restrict: 'A', 
     controller: 'YourWidgetCtrl'
  }
)
<!-- Basic component template structure -->

<div your-widget>
  <!-- edit widget view -->
  <div ng-show="widget.isEditMode" class="edit">
    <h4>Widget settings</h4>
    
    <!-- settings directive for managing organizations (widget data come from multiple companies) -->
    <div setting-organizations parent-widget="widget" class="part" deferred="::orgDeferred" />
    
    <!-- actions -->
    <div class="bottom-buttons" align="right">
      <button class="btn btn-default" ng-click="initSettings()">Cancel</button>
      <button class="btn btn-warning" ng-click="updateSettings()">Save</button>
    </div>
  </div>
  <!-- widget view -->
  <div ng-hide="widget.isEditMode">
    <!-- controller bound boolean for switching between widget and 'data not found' message -->
    <div ng-show="(isDataFound==true)">

      <!-- widget content -->

    </div>
    <!-- data not found -->
    <div ng-if="(isDataFound==false)" common-data-not-found on-display-alerts="onDisplayAlerts()" widget-engine="::widget.category" widget-width="::widget.width"/>
  </div>
</div>
  1. Start implementing the widget's controller.

It must contain at least the following elements for a widget without a chart: - settingsPromises, which is an array of promises, contains a promise for each custom sub-directive that you add to your widget (e.g: a setting, a chart...). It is essential that you pass a deferred object (initialized by $q.defer()) to each setting or chart that you want to add to your widget: it will be used to make sure the setting is properly initialized before the widget can call its functions. - $scope.widget.initContext() is the function that will be called just after the widget has retrieved its content from the API. It should be implemented, and used to determine if the widget can be displayed properly, and to initialize potential specific variables.

   w = $scope.widget
  
   # Define settings
   # --------------------------------------
   $scope.orgDeferred = $q.defer()
  
   settingsPromises = [
     $scope.orgDeferred.promise
   ]
  
   # Widget specific methods
   # --------------------------------------
   w.initContext = ->
     $scope.isDataFound = w.content? 
  • If your widget is using a chart:
    • w.format() will be required to build the chart. It will be triggered by the ImpacWidgets service show method, after the data has been successfully retrieved from Impac!.
  $scope.drawTrigger = $q.defer()
  
  ...
  
  w.format = ->
    
    ...
    
    # formats the widget content in data that will be readable by Chartjs
    # See other widgets directives for examples of different chart types, 
    # arguments needed etc. Also take a look at the ChartFormatterSvc methods.
    chartData = ChartFormatterSvc.lineChart([inputData],options)
    # passes chartData to the chart directive, and calls chart.draw()
    $scope.drawTrigger.notify(chartData)
  <div your-widget>
    ...
    
    <div impac-chart draw-trigger="::drawTrigger.promise" deferred="::chartDeferred"></div>
    ...
  </div>
    
  1. Notify the widget's main directive that the widget's specific context has been loaded and is ready. To do that, we use a deferred object that is initialized in the main directive (widget.directive.coffee), and resolved at the end of the specific directive (accounts-my-new-widget.directive.coffee):
...

$scope.widgetDeferred.resolve(settingsPromises)

IMPORTANT: The settingsPromises array defined in 1/ has to be passed back to the main directive to make sure it will wait for all the settings to be initialized before calling the widget's #show function.

  1. Add the new components angular module to the src/impac-angular.module.js module declarations.
  angular.module('impac.components.widgets',
    [
      'impac.components.widgets.your-widget'
    ]
  );
  1. Rebuild via gulp or gulp serve or gulp workspace, and then you should be able to add your new widget to a dashboard!

How-to: Create a setting


Conventions specific to settings development
  • A 'setting' is a directive that may be reused by any widget. The purpose of any setting is to handle the management of one 'metadata parameter', which will define the widget configuration. Basically, everytime a configuration information has to be saved before next dashboard reload, a setting should be used.

  • Avoid using the $scope.parentWidget inside of the setting's controller: when you have to call a method belonging to the widget object, pass a callback to the directive as an argument. When you need to access some data contained into $scope.parentWidget.content, try passing an object to the directive as well. Eg:

    scope: {
      parentWidget: '='
      deferred: '='
      callBackToWidget: '=onActivate'
      widgetContentData: '=data'
    }
Process
  1. Create the setting's files:
  • in /src/components/widgets-settings/, add a folder 'setting-name' (e.g: my-new-setting).
  • in this new folder, add three files:
    • my-new-setting.directive.coffee containing the angular directive and controller defining your setting behaviour.
    • my-new-setting.tmpl.html containing the template of your setting.
    • my-new-setting.less containing the stylesheet of your setting.
  1. Define your setting's directive. It requires at least the following attributes:
scope: {
  parentWidget: '=' // widget object containing the setting object 
  deferred: '=' // deferred object that will be resolved once the setting context is loaded
}
  1. Start implementing your setting's controller:
  • create a setting object with a unique identifier:
    setting = {}
    setting.key = "my-new-setting"
    • implement the setting.initialize() function, which must be used to set the setting's default parameters
    • implement the setting.toMetadata() function, which will be called when the setting content has to be stored in the Maestrano config. It must return a javascript hash that will be directly stored into widget.metadata. For instance, if setting.toMetadata() returns { my_new_setting: true }, once the widget is updated, it will contain: widget.metadata.my_new_setting = true
  1. Push the setting in the parent widget settings list: $scope.widget.settings.push(setting)

  2. Notify the parent widget that the setting's context has been loaded and is ready: $scope.deferred.resolve(setting). IMPORTANT: The parent widget's #show method (= call to the Impac! API to retrieve the widget's content) will be called only once all the settings are loaded (= once they have resolved their $scope.deferred object).

Code Conventions across impac-angular


General

  • HTML Templates must not use double-quotes for strings (I'm looking at you, Ruby devs). Only html attribute values may be wrapped in double qoutes.

    • REASON: when gulp-angular-templatecache module works its build magic, having double quotes within double quotes breaks the escaping.
  • We have found this angular style guide to be an excellent reference which outlines good ways to write angular. I try to write CoffeeScript so it compiles in line with this style guide.

File Naming

  • Slug style file naming, e.g this-is-slug-style.
  • Add filename extensions to basename describing the type of component it is.
  // good
  some-file.svc.coffee
  some-file.modal.html

  // bad
  some-file-svc.coffee
  some-file-modal.html  

**IMPORTANT:** Widget folder and file names must be the same as the widget's category that is stored in the back-end, for example:
  // widget data returned from maestrano database
  widget: {
    category: "invoices/aged_payables_receivables",
    ...
  }

Component folder & file name should be:

  components/invoices-aged-payables-receivables/invoices-aged-payables-receivables.directive.coffee

Stylesheets

The goal is to be able to work on a specific component / piece of functionality and be able to quickly isolate the javascript and css without having to dig through a 1000 line + css / js file, and also preventing styles from bleeding.

Stylesheets should be kept within the components file structure, with styles concerning that component.

Only main stylesheets should be kept in the stylesheets folder, like variables.less, global.less, and mixins.less, etc.

Component specific styles should be wrapped in a containing ID to prevent bleeding.

With widgets, there is no need for creating an id for nesting styles within. There is some code in place which adds the class dynamically to the template from the Widget's template data retrieved from the API.

To view how this works, see files components/src/widget/widget.html and component/src/widget/widget.directive.coffee.

Below is an example of the correct less closure for your widgets components less files.

  // impac-angular/src/components/widgets/sales-list/sales-list.less
  .analytics .widget-item .content.sales-list {
    ul {}
  }

With other components / widgets settings components, your less should be closured like below.

  // components/your-component-category/your-component.less
  .analytics .your-component-category.your-component {
    /* styles that wont bleed and are easily identifiable as only within this component */
    ul {}
  }

Template to match above:

  <!-- components/your-component-category/your-component.tmpl.html -->
  <div class"your-component-category your-component">
    <!-- html template for component -->
  </div>

During the build process gulp will inject @import declarations from .less files in components/ into src/impac-angular.less, concatinate all less files into dist/impac-angular.less, and compile and minify all less files into dist/impac-angular.css and dist/impac-angular.min.css.

Tests

Test should be created within service or component folders. Just be sure to mark them with a .spec extension.

Example:

  components/
    some-component/
      some-component.directive.coffee
      some-component.spec.js
  services/
    some-service/
      some-service.service.coffee
      some-service.spec.js

To run tests, first build impac-angular with gulp build. Then run gulp test.

Gulp tasks

  • gulp or gulp serve will spin up a server, wiredep workspace/index.html, run a gulp build, and start a watch that will trigger build when any workspace/ or src/ files change.
  • gulp serve:noreload do the same as above, but without the watch task.
  • gulp build will build all /dist files.
  • gulp build:dist will run a gulp clean first, then build all /dist files, ensure only the current src files are included in dist (especially relevant for images).
  • gulp workspace will inject all dependencies with wiredep, and run a gulp build.
  • gulp test will run unit-tests on dist/impac-angular.js and dist/impac-angular.min.js

Licence

Copyright 2015 Maestrano Pty Ltd