Add tableActions component

Creating a separate table actions component to allow
row action customizations like disabling certain
actions and rendering different icons.

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I85e96045af27701f5ecc4af9bf824e248abccbf5
diff --git a/app/common/components/table/table-actions.js b/app/common/components/table/table-actions.js
new file mode 100644
index 0000000..e1e47ec
--- /dev/null
+++ b/app/common/components/table/table-actions.js
@@ -0,0 +1,91 @@
+window.angular && (function(angular) {
+  'use strict';
+
+  /**
+   *
+   * tableActions Component
+   *
+   * To use:
+   * The <table-actions> component expects an 'actions' attribute
+   * that should be an array of action objects.
+   * Each action object should have 'type', 'enabled', and 'file'
+   * properties.
+   *
+   * actions: [
+   *  {type: 'Edit', enabled: true, file: 'icon-edit.svg'},
+   *  {type: 'Delete', enabled: false, file: 'icon-trashcan.svg'}
+   * ]
+   *
+   * The 'type' property is a string value that will be emitted to the
+   * parent component when clicked.
+   *
+   * The 'enabled' property is a boolean that will enable/disable
+   * the button.
+   *
+   * The 'file' property is a string value of the filename of the svg icon
+   * to provide <icon> directive.
+   *
+   */
+
+  const controller = function() {
+    /**
+     * Set defaults if properties undefined
+     * @param {[]} actions
+     */
+    const setActions = (actions = []) => {
+      return actions
+          .map((action) => {
+            if (action.type === undefined) {
+              return;
+            }
+            if (action.file === undefined) {
+              action.file = null;
+            }
+            if (action.enabled === undefined) {
+              action.enabled = true;
+            }
+            return action;
+          })
+          .filter((action) => action);
+    };
+
+    /**
+     * Callback when button action clicked
+     * Emits the action type to the parent component
+     */
+    this.onClick = (action) => {
+      this.emitAction({action});
+    };
+
+    /**
+     * onInit Component lifecycle hook
+     */
+    this.$onInit = () => {
+      this.actions = setActions(this.actions);
+    };
+  };
+
+  /**
+   * Component template
+   */
+  const template = `
+    <button
+      class="btn  btn-tertiary"
+      type="button"
+      aria-label="{{action.type}}"
+      ng-repeat="action in $ctrl.actions"
+      ng-disabled="!action.enabled"
+      ng-click="$ctrl.onClick(action.type)">
+      <icon ng-if="action.file !== null" ng-file="{{action.file}}"></icon>
+      <span ng-if="action.file === null">{{action.type}}</span>
+    </button>`
+
+  /**
+   * Register tableActions component
+   */
+  angular.module('app.common.components').component('tableActions', {
+    controller,
+    template,
+    bindings: {actions: '<', emitAction: '&'}
+  })
+})(window.angular);
\ No newline at end of file
diff --git a/app/common/components/table/table.html b/app/common/components/table/table.html
index 6ec520c..b40c346 100644
--- a/app/common/components/table/table.html
+++ b/app/common/components/table/table.html
@@ -19,13 +19,12 @@
         {{item}}
       </td>
       <!-- Row Actions -->
-      <td ng-if="$ctrl.model.actions.length > 0"
+      <td ng-if="$ctrl.rowActionsEnabled"
           class="bmc-table__cell  bmc-table__row-actions">
-        <button ng-repeat="action in $ctrl.model.actions"
-                ng-click="$ctrl.onClickAction(action, row);"
-                class="btn  btn-tertiary">
-          {{action}}
-        </button>
+        <table-actions
+          actions="row.actions"
+          emit-action="$ctrl.onEmitTableAction(action, row)">
+        </table-actions>
       </td>
     </tr>
     <!-- Empty table -->
diff --git a/app/common/components/table/table.js b/app/common/components/table/table.js
index 2d7fc77..09a6d6d 100644
--- a/app/common/components/table/table.js
+++ b/app/common/components/table/table.js
@@ -3,19 +3,24 @@
 
   /**
    *
-   * Controller for bmcTable Component
+   * bmcTable Component
    *
    * To use:
    * The <bmc-table> component expects a 'model' attribute
    * that will contain all the data needed to render the table.
    *
-   * The model object should contain 'header', 'data', and 'actions'
+   * The component also accepts a 'row-actions-enabled' attribute,
+   * to optionally render table row actions. Defaults to false.
+   * Pass true to render actions. Row actions are defined in
+   * model.data.actions.
+   *
+   *
+   * The model object should contain 'header', and 'data'
    * properties.
    *
    * model: {
-   *    header: <string>[],   // Array of header labels
-   *    data: <any>[],        // Array of each row object
-   *    actions: <string>[]   // Array of action labels
+   *    header: <string>[],  // Array of header labels
+   *    data: <any>[],       // Array of each row object
    * }
    *
    * The header property will render each label as a <th> in the table.
@@ -24,13 +29,15 @@
    * Each row object in the model.data array should also have a 'uiData'
    * property that should be an array of the properties that will render
    * as each table cell <td>.
+   * Each row object in the model.data array can optionally have an
+   * 'actions' property that should be an array of actions to provide the
+   * <bmc-table-actions> component.
    *
-   * The actions property will render into clickable buttons at the end
-   * of each row.
-   * When a user clicks an action button, the component
-   * will emit the action label with the associated row object.
+   * The 'rowActionsEnabled' property will render <bmc-table-actions> if set
+   * to true.
    *
    */
+
   const TableController = function() {
     /**
      * Init model data
@@ -46,14 +53,6 @@
         }
         return row;
       })
-      model.actions = model.actions === undefined ? [] : model.actions;
-
-      if (model.actions.length > 0) {
-        // If table actions were provided, push an empty
-        // string to the header array to account for additional
-        // table actions cell
-        model.header.push('');
-      }
       return model;
     };
 
@@ -64,7 +63,7 @@
      * @param {string} action : action type
      * @param {any} row : user object
      */
-    this.onClickAction = (action, row) => {
+    this.onEmitTableAction = (action, row) => {
       if (action !== undefined && row !== undefined) {
         const value = {action, row};
         this.emitAction({value});
@@ -75,7 +74,19 @@
      * onInit Component lifecycle hooked
      */
     this.$onInit = () => {
+      if (this.model === undefined) {
+        console.log('<bmc-table> Component is missing "model" attribute.');
+        return;
+      }
       this.model = setModel(this.model);
+      this.rowActionsEnabled =
+          this.rowActionsEnabled === undefined ? false : true;
+      if (this.rowActionsEnabled) {
+        // If table actions are enabled push an empty
+        // string to the header array to account for additional
+        // table actions cell
+        this.model.header.push('');
+      }
     };
   };
 
@@ -85,6 +96,6 @@
   angular.module('app.common.components').component('bmcTable', {
     template: require('./table.html'),
     controller: TableController,
-    bindings: {model: '<', emitAction: '&'}
+    bindings: {model: '<', rowActionsEnabled: '<', emitAction: '&'}
   })
 })(window.angular);
diff --git a/app/common/directives/icon-provider.js b/app/common/directives/icon-provider.js
index 5554fdd..bee6150 100644
--- a/app/common/directives/icon-provider.js
+++ b/app/common/directives/icon-provider.js
@@ -13,8 +13,9 @@
 
   angular.module('app.common.directives').directive('icon', () => {
     return {
-      restrict: 'E', link: (scope, element, attrs) => {
-        const file = attrs.file;
+      restrict: 'E',
+      link: (scope, element, attrs) => {
+        const file = attrs.file || attrs.ngFile;
         if (file === undefined) {
           console.log('File name not provided for <icon> directive.')
           return;
@@ -23,6 +24,6 @@
         element.html(svg);
         element.addClass('icon');
       }
-    }
+    };
   })
 })(window.angular);
\ No newline at end of file
diff --git a/app/common/styles/components/table.scss b/app/common/styles/components/table.scss
index 17df264..40b6a64 100644
--- a/app/common/styles/components/table.scss
+++ b/app/common/styles/components/table.scss
@@ -161,10 +161,14 @@
 }
 
 .bmc-table__cell {
-  padding: 4px 16px;
+  padding: 10px 16px;
   background-color: $base-02--07;
 }
 
 .bmc-table__row-actions {
   text-align: right;
+  .btn {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
 }
\ No newline at end of file
diff --git a/app/index.js b/app/index.js
index 32acb66..c839708 100644
--- a/app/index.js
+++ b/app/index.js
@@ -67,6 +67,7 @@
 
 import components_index from './common/components/index.js';
 import table_component from './common/components/table/table.js';
+import table_actions_component from './common/components/table/table-actions.js';
 
 import login_index from './login/index.js';
 import login_controller from './login/controllers/login-controller.js';
diff --git a/app/users/controllers/user-accounts-controller.html b/app/users/controllers/user-accounts-controller.html
index fd6a28c..41094b8 100644
--- a/app/users/controllers/user-accounts-controller.html
+++ b/app/users/controllers/user-accounts-controller.html
@@ -23,6 +23,7 @@
       </div>
       <bmc-table
         model="tableModel"
+        row-actions-enabled="true"
         emit-action="onEmitAction(value)"
         class="local-users__table">
       </bmc-table>
diff --git a/app/users/controllers/user-accounts-controller.js b/app/users/controllers/user-accounts-controller.js
index 3d63d69..b5ed1a5 100644
--- a/app/users/controllers/user-accounts-controller.js
+++ b/app/users/controllers/user-accounts-controller.js
@@ -19,16 +19,24 @@
 
       $scope.tableModel = {};
       $scope.tableModel.data = [];
-      $scope.tableModel.header = ['Username', 'Privilege', 'Account status']
-      $scope.tableModel.actions = ['Edit', 'Delete'];
+      $scope.tableModel.header = ['Username', 'Privilege', 'Account status'];
 
       /**
        * Data table mapper
        * @param {*} user
+       * @returns user
        */
       function mapTableData(user) {
-        let accountStatus =
+        const accountStatus =
             user.Locked ? 'Locked' : user.Enabled ? 'Enabled' : 'Disabled';
+        const editAction = {type: 'Edit', enabled: true, file: 'icon-edit.svg'};
+        const deleteAction = {
+          type: 'Delete',
+          enabled: user.UserName === 'root' ? false : true,
+          file: 'icon-trashcan.svg'
+        };
+
+        user.actions = [editAction, deleteAction];
         user.uiData = [user.UserName, user.RoleId, accountStatus];
         return user;
       }