Skip to content

Latest commit

 

History

History
461 lines (338 loc) · 20.7 KB

DEVELOPER.md

File metadata and controls

461 lines (338 loc) · 20.7 KB

Impac Developer Toolkit

Steps to get an Impac! Workspace loaded:

Before getting started, we recommend you read the "Backend Ecosystem" section just below.

  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 file in the /src or /workspace directory will automatically reload your Developer Workspace, applying your code. (Note that unfortunately the reload time can take up to 8 seconds to finish... This is on our todo list.)
Generating test data

The best way to populate the Widgets and KPIs with data, is to add an App to your Dashboard. We recommend generating a demo account with Xero.

Steps to add a Xero demo account to your Dashboard:

  1. Navigate to Impac Express: https://impac-mnoe-uat.maestrano.io.
  2. Log in with your same details used to register with the Developer Toolkit.
  3. Access your Dashboard.
  4. At the top-center of the screen, there is an "App dock" (similar to a Macintosh app dock) with a plus icon. Clicking that will take you to the Marketplace (#!/marketplace).
  5. Here, search for & select "Xero".
  6. Click the "Launch App button".
  7. This will redirect you to Xero, at the bottom of the screen click on the link "Don't have a login? Try Xero for free". Follow the instructions to register your account (no need to use real information here), you will need to complete an email confirmation.
  8. Once redirected back to Xero from the email confirmation action, create your password, then, on the next screen, it is important that you click the "Try the Demo Company" link at the bottom of the "Add your organisation to start using Xero now" screen!.
  9. Now youe Xero account is setup, you can sync the data that has been generated by the Xero Demo Account, and you can also add more records later on to add to your dataset.
  10. Navigate back to your Dashboard on Impac Express, and you should see the Xero app added to your App Dock.
  11. To force a sync, click the Sync Data button that is on the left of the Currency Selector on your dashboard (you will need to have added at least one dashboard to see this element).

Backend Ecosystem

There are a few backend APIs which impac-angular interacts with. It is important to have a bit of an understanding on this before developing on Impac! Angular.

Maestrano Express

Maestrano Express is a heavily customisatable Rails app instance (bootstrapped by a Maestrano Rails Engine) which hosts all the Maestrano products.

https://impac-mnoe-uat.maestrano.io named "Impac! Express", is a Maestrano Express app that has been bootstraped and hosted just for Impac! development and testing.

This Impac! Angular Developer Workspace is configured to query Impac! Express which mostly (for the Impac! endpoints) redirects queries to "Maestrano Hub".

Maestrano Hub is an API which exposes the core Maestrano APIs to manage organizations, users, applications, dashboards, widgets etc.

If you navigate to Impac! Express, you can sign in with the same account you registered on the developer toolkit login with, and you will be accessing the same backend data. The only difference is the front-end is served via deployed public static JS assets. So for example, a distributed version of Impac! Angular.

This is very helpful for accessing the full range of features, associated to the same account you are developing with for Impac! Angular (e.g integrating an App and running a data sync). Or, for testing a stable state of the front-end agaist works you have been doing.

Impac!

Impac! is a Rails API which acts as a reporting server, retrieving raw data (from another Maestrano micro-service called "Connec!"), calculating metrics / analytics, and formatting the reports into json responses for front-end consumers.

The Impac! Angular front-end mainly queries Impac! retrieving data to display in Widgets & KPIs.

How the Workspace app works

The /workspace directory serves as a mini angular app, which includes the Impac! Angular library as a Bower dependency, and loads it using Angular's module dependency system.

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

The impacWorkspace bower dependencies are injected using Gulp Wiredep into the /workspace/index.html, and is then served to the browser. The /workspace and /src directory are watched for changes.

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()" endpoint="::widget.category" 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).

  3. Add the new components angular module to the src/impac-angular.module.js module declarations.

  angular.module('impac.components.widgets-settings',
    [
      'impac.components.widgets-settings.my-new-setting'
    ]
  );

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