Simplified form

Change-Id: Ib4b17823be86e18bd5e83679cde7fc95a4f8bac1
diff --git a/src/app/core/form/form-helpers.ts b/src/app/core/form/form-helpers.ts
index 0677cbd..d2d7373 100644
--- a/src/app/core/form/form-helpers.ts
+++ b/src/app/core/form/form-helpers.ts
@@ -1,10 +1,11 @@
-import * as _ from 'lodash';
 import {IXosConfigHelpersService} from '../services/helpers/config.helpers';
+import {IXosFormInput} from './form';
 
 export interface IXosFormHelpersService {
   _getFieldFormat(value: any): string;
   parseModelField(fields: any): any[];
   buildFormStructure(modelField: any[], customField: any[], model: any, order: string[]): any;
+  buildFormData(fields: IXosFormInput[], model: any): any;
 }
 
 export class XosFormHelpers {
@@ -54,53 +55,5 @@
 
     return typeof value;
   };
-
-  public buildFormStructure = (modelField, customField, model, order) => {
-    // TODO take an array as input
-    // NOTE do we want to support auto-generated forms??
-    // We can take that out of this component and autogenerate the config somewhere else
-    const orderedForm = {};
-
-    modelField = angular.extend(modelField, customField);
-    customField = customField || {};
-
-    if (order) {
-      _.each(order, function (key: string) {
-        orderedForm[key] = {};
-      });
-    }
-
-    _.each(Object.keys(modelField), (f) => {
-
-      orderedForm[f] = {
-        label: (customField[f] && customField[f].label) ? `${customField[f].label}:` : this.ConfigHelpers.toLabel(f),
-        type: (customField[f] && customField[f].type) ? customField[f].type : this._getFieldFormat(model[f]),
-        validators: (customField[f] && customField[f].validators) ? customField[f].validators : {},
-        hint: (customField[f] && customField[f].hint) ? customField[f].hint : '',
-      };
-
-      if (customField[f] && customField[f].options) {
-        orderedForm[f].options = customField[f].options;
-      }
-      if (customField[f] && customField[f].properties) {
-        orderedForm[f].properties = customField[f].properties;
-      }
-      if (orderedForm[f].type === 'date') {
-        model[f] = new Date(model[f]);
-      }
-
-      if (orderedForm[f].type === 'number') {
-        model[f] = parseInt(model[f], 10);
-      }
-    });
-
-    return orderedForm;
-  };
-
-  public parseModelField = (fields) => {
-  return _.reduce(fields, (form, f) => {
-    form[f] = {};
-    return form;
-  }, {});
 }
-}
+
diff --git a/src/app/core/form/form.html b/src/app/core/form/form.html
index a583269..2172d3a 100644
--- a/src/app/core/form/form.html
+++ b/src/app/core/form/form.html
@@ -1,8 +1,11 @@
 <form name="vm.{{vm.config.formName || 'form'}}" novalidate>
-    <div class="form-group" ng-repeat="(name, field) in vm.formField">
-        <xos-field name="name" field="field" ng-model="vm.ngModel[name]"></xos-field>
-        <xos-validation field="vm[vm.config.formName || 'form'][name]" form = "vm[vm.config.formName || 'form']"></xos-validation>
-        <div class="alert alert-info" ng-show="(field.hint).length >0" role="alert">{{field.hint}}</div>
+    <!--<div class="form-group" ng-repeat="(name, field) in vm.formField">-->
+        <!--<xos-field name="name" field="field" ng-model="vm.ngModel[name]"></xos-field>-->
+        <!--<xos-validation field="vm[vm.config.formName || 'form'][name]" form = "vm[vm.config.formName || 'form']"></xos-validation>-->
+        <!--<div class="alert alert-info" ng-show="(field.hint).length >0" role="alert">{{field.hint}}</div>-->
+    <!--</div>-->
+    <div class="form-group" ng-repeat="field in vm.config.inputs">
+        <xos-field name="field.name" field="field" ng-model="vm.ngModel[field.name]"></xos-field>
     </div>
     <div class="form-group" ng-if="vm.config.actions">
         <!--<xos-alert config="vm.config.feedback" show="vm.config.feedback.show">{{vm.config.feedback.message}}</xos-alert>-->
@@ -15,4 +18,4 @@
             {{action.label}}
         </button>
     </div>
-</form>
\ No newline at end of file
+</form>
diff --git a/src/app/core/form/form.ts b/src/app/core/form/form.ts
index a5cfa67..6713fe7 100644
--- a/src/app/core/form/form.ts
+++ b/src/app/core/form/form.ts
@@ -4,6 +4,45 @@
 import {IXosFormHelpersService} from './form-helpers';
 import {IXosConfigHelpersService} from '../services/helpers/config.helpers';
 
+export interface IXosFormAction {
+  label: string;
+  icon: string;
+  class: string;
+  cb(item: any, form: any): void;
+}
+
+export interface IXosFeedback {
+  show: boolean;
+  message: string;
+  type: string; // NOTE is possible to enumerate success, error, warning, info?
+}
+
+export interface IXosFormInputValidator {
+  minlength?: number;
+  maxlength?: number;
+  required?: boolean;
+  min?: number;
+  max?: number;
+  custom?(value: any): any;
+    // do your validation here and return true | false
+    // alternatively you can return an array [errorName, true|false]
+}
+
+export interface IXosFormInput {
+  name: string;
+  label: string;
+  type: string; // options are: [date, boolean, number, email, string, select],
+  validators: IXosFormInputValidator;
+}
+
+export interface IXosFormConfig {
+  exclude?: string[];
+  actions: IXosFormAction[];
+  feedback?: IXosFeedback;
+  inputs: IXosFormInput[];
+  formName: string;
+}
+
 class FormCtrl {
   $inject = ['$onInit', '$scope', 'XosFormHelpers', 'ConfigHelpers'];
 
@@ -17,7 +56,6 @@
     private XosFormHelpers: IXosFormHelpersService,
     private ConfigHelpers: IXosConfigHelpersService
   ) {
-
   }
 
   $onInit() {
@@ -29,6 +67,11 @@
       throw new Error('[xosForm] Please provide an action list in the configuration');
     }
 
+    if (!this.config.formName) {
+      throw new Error('[xosForm] Please provide a formName property in the config');
+    }
+
+    // NOTE is needed ??
     if (!this.config.feedback) {
       this.config.feedback =  {
         show: false,
@@ -37,36 +80,8 @@
       };
     }
 
-    // TODO Define this list in a service (eg: ConfigHelper)
-    this.excludedField = this.ConfigHelpers.excluded_fields;
-    if (this.config && this.config.exclude) {
-      this.excludedField = this.excludedField.concat(this.config.exclude);
-    }
-
-    this.formField = [];
-
-    this.$scope.$watch(() => this.config, () => {
-      if (!this.ngModel) {
-        return;
-      }
-      let diff = _.difference(Object.keys(this.ngModel), this.excludedField);
-      let modelField = this.XosFormHelpers.parseModelField(diff);
-      this.formField = this.XosFormHelpers.buildFormStructure(modelField, this.config.fields, this.ngModel, this.config.order);
-    }, true);
-
-    this.$scope.$watch(() => this.ngModel, (model) => {
-      console.log(this.ngModel);
-      // empty from old stuff
-      this.formField = {};
-      if (!model) {
-        return;
-      }
-      let diff = _.difference(Object.keys(model), this.excludedField);
-      let modelField = this.XosFormHelpers.parseModelField(diff);
-      this.formField = this.XosFormHelpers.buildFormStructure(modelField, this.config.fields, model, this.config.order);
-      console.log(this.formField);
-    });
-
+    // remove excluded inputs
+    _.remove(this.config.inputs, i => this.config.exclude.indexOf(i.name) > -1);
   }
 }
 
diff --git a/src/app/core/services/helpers/config.helpers.spec.ts b/src/app/core/services/helpers/config.helpers.spec.ts
index 432bd7e..beec2e0 100644
--- a/src/app/core/services/helpers/config.helpers.spec.ts
+++ b/src/app/core/services/helpers/config.helpers.spec.ts
@@ -6,6 +6,7 @@
 import {xosCore} from '../../index';
 import {IModeldef} from '../../../datasources/rest/modeldefs.rest';
 import {IXosTableCfg} from '../../table/table';
+import {IXosFormInput, IXosFormConfig} from '../../form/form';
 
 let service: IXosConfigHelpersService;
 
@@ -20,12 +21,24 @@
     {
       type: 'string',
       name: 'name',
-      validators: {}
+      validators: {
+        required: true
+      }
     },
     {
       type: 'string',
       name: 'something',
-      validators: {}
+      validators: {
+        maxlength: 30
+      }
+    },
+    {
+      type: 'number',
+      name: 'else',
+      validators: {
+        min: 20,
+        max: 40
+      }
     },
     {
       type: 'date',
@@ -51,6 +64,7 @@
       expect(service.pluralize('test', 1)).toEqual('test');
       expect(service.pluralize('xos')).toEqual('xosses');
       expect(service.pluralize('slice')).toEqual('slices');
+      expect(service.pluralize('Slice', 1)).toEqual('Slice');
     });
 
     it('should preprend count to string', () => {
@@ -104,7 +118,11 @@
       expect(cols[2].prop).toBe('something');
       expect(cols[2].link).not.toBeDefined();
 
-      expect(cols[3]).not.toBeDefined();
+      expect(cols[3].label).toBe('Else');
+      expect(cols[3].prop).toBe('else');
+      expect(cols[3].link).not.toBeDefined();
+
+      expect(cols[4]).not.toBeDefined();
     });
   });
 
@@ -118,8 +136,47 @@
     });
   });
 
+  describe('the modelFieldToInputConfig', () => {
+    it('should return an array of inputs', () => {
+      const inputs: IXosFormInput[] = service.modelFieldToInputCfg(model.fields);
+      expect(inputs[0].name).toBe('id');
+      expect(inputs[0].type).toBe('number');
+      expect(inputs[0].label).toBe('Id');
+
+      expect(inputs[1].name).toBe('name');
+      expect(inputs[1].type).toBe('string');
+      expect(inputs[1].label).toBe('Name');
+      expect(inputs[1].validators.required).toBe(true);
+
+      expect(inputs[2].name).toBe('something');
+      expect(inputs[2].type).toBe('string');
+      expect(inputs[2].label).toBe('Something');
+      expect(inputs[2].validators.maxlength).toBe(30);
+
+      expect(inputs[3].name).toBe('else');
+      expect(inputs[3].type).toBe('number');
+      expect(inputs[3].label).toBe('Else');
+      expect(inputs[3].validators.min).toBe(20);
+      expect(inputs[3].validators.max).toBe(40);
+    });
+  });
+
+  describe('the modelToFormCfg method', () => {
+    it('should return a form config', () => {
+      const config: IXosFormConfig = service.modelToFormCfg(model);
+      expect(config.formName).toBe('TestForm');
+      expect(config.actions.length).toBe(1);
+      expect(config.actions[0].label).toBe('Save');
+      expect(config.actions[0].class).toBe('success');
+      expect(config.actions[0].icon).toBe('ok');
+      expect(config.actions[0].cb).toBeDefined();
+      expect(config.inputs.length).toBe(4);
+    });
+  });
+
   it('should convert a core model name in an URL', () => {
     expect(service.urlFromCoreModel('Slice')).toBe('/core/slices');
     expect(service.urlFromCoreModel('Xos')).toBe('/core/xosses');
   });
 });
+
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
index fc836e7..6b89856 100644
--- a/src/app/core/services/helpers/config.helpers.ts
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -2,6 +2,7 @@
 import * as pluralize from 'pluralize';
 import {IXosTableColumn, IXosTableCfg} from '../../table/table';
 import {IModeldef} from '../../../datasources/rest/modeldefs.rest';
+import {IXosFormConfig, IXosFormInput} from '../../form/form';
 
 export interface IXosModelDefsField {
   name: string;
@@ -11,8 +12,10 @@
 
 export interface IXosConfigHelpersService {
   excluded_fields: string[];
-  modelToTableCfg(model: IModeldef, baseUrl: string): IXosTableCfg;
   modelFieldsToColumnsCfg(fields: IXosModelDefsField[], baseUrl: string): IXosTableColumn[]; // TODO use a proper interface
+  modelToTableCfg(model: IModeldef, baseUrl: string): IXosTableCfg;
+  modelFieldToInputCfg(fields: IXosModelDefsField[]): IXosFormInput[];
+  modelToFormCfg(model: IModeldef): IXosFormConfig;
   pluralize(string: string, quantity?: number, count?: boolean): string;
   toLabel(string: string, pluralize?: boolean): string;
   toLabels(string: string[], pluralize?: boolean): string[];
@@ -35,7 +38,9 @@
     'omf_friendly',
     'enabled',
     'validators',
-    'password'
+    'password',
+    'backend_need_delete',
+    'backend_need_reap'
   ];
 
   constructor(
@@ -43,6 +48,7 @@
   ) {
     pluralize.addIrregularRule('xos', 'xosses');
     pluralize.addPluralRule(/slice$/i, 'slices');
+    pluralize.addSingularRule(/slice$/i, 'slice');
   }
 
   public pluralize(string: string, quantity?: number, count?: boolean): string {
@@ -143,6 +149,41 @@
     return `/core/${this.pluralize(name.toLowerCase())}`;
   }
 
+  public modelFieldToInputCfg(fields: IXosModelDefsField[]): IXosFormInput[] {
+
+    return _.map(fields, f => {
+      return {
+        name: f.name,
+        label: this.toLabel(f.name),
+        type: f.type,
+        validators: f.validators
+      };
+    })
+      .filter(f => this.excluded_fields.indexOf(f.name) === -1);
+  }
+
+  public modelToFormCfg(model: IModeldef): IXosFormConfig {
+    return {
+      formName: `${model.name}Form`,
+      exclude: ['backend_status'],
+      actions: [{
+        label: 'Save',
+        class: 'success',
+        icon: 'ok',
+        cb: (item, form) => {
+          item.$save()
+            .then(res => {
+              this.toastr.success(`${item.name} succesfully saved`);
+            })
+            .catch(err => {
+              this.toastr.error(`Error while saving ${item.name}`);
+            });
+        }
+      }],
+      inputs: this.modelFieldToInputCfg(model.fields)
+    };
+  }
+
   private fromCamelCase(string: string): string {
     return string.split(/(?=[A-Z])/).map(w => w.toLowerCase()).join(' ');
   }
@@ -159,4 +200,3 @@
     return string.slice(0, 1).toUpperCase() + string.slice(1);
   }
 }
-
diff --git a/src/app/views/crud/crud.html b/src/app/views/crud/crud.html
index 7c80b92..f5f1f76 100644
--- a/src/app/views/crud/crud.html
+++ b/src/app/views/crud/crud.html
@@ -5,6 +5,7 @@
                 <div class="view-header">
                     <div class="pull-right text-right" style="line-height: 14px">
                         <!--<small>UI Elements<br>General<br> <span class="c-white">Grid system</span></small>-->
+                        <a class="btn btn-default" ng-if="vm.list" href="{{vm.baseUrl}}add">Add</a>
                         <a class="btn btn-default" ng-if="!vm.list" href="{{vm.baseUrl}}">Back</a>
                     </div>
                     <div class="header-icon">
@@ -20,7 +21,7 @@
                 <hr>
             </div>
         </div>
-        <div class="row" ng-show="vm.related.length > 0">
+        <div class="row" ng-show="vm.related.length > 0 && vm.model.id">
             <div class="view-header">
                 <div class="col-lg-4">
                     <h4>Related Items: </h4>
@@ -47,4 +48,4 @@
             <xos-form ng-model="vm.model" config="vm.formCfg"></xos-form>
         </div>
     </div>
-</section>
\ No newline at end of file
+</section>
diff --git a/src/app/views/crud/crud.ts b/src/app/views/crud/crud.ts
index 1d4eccd..f3cc1fa 100644
--- a/src/app/views/crud/crud.ts
+++ b/src/app/views/crud/crud.ts
@@ -2,14 +2,17 @@
 import {IModelStoreService} from '../../datasources/stores/model.store';
 import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
 import * as _ from 'lodash';
+import {IXosFormConfig} from '../../core/form/form';
+import {IXosResourceService} from '../../datasources/rest/model.rest';
 export interface IXosCrudData {
   model: string;
   related: string[];
   xosTableCfg: IXosTableCfg;
+  xosFormCfg: IXosFormConfig;
 }
 
 class CrudController {
-  static $inject = ['$scope', '$state', '$stateParams', 'ModelStore', 'ConfigHelpers'];
+  static $inject = ['$scope', '$state', '$stateParams', 'ModelStore', 'ConfigHelpers', 'ModelRest'];
 
   public data: IXosCrudData;
   public tableCfg: IXosTableCfg;
@@ -27,7 +30,8 @@
     private $state: angular.ui.IStateService,
     private $stateParams: ng.ui.IStateParamsService,
     private store: IModelStoreService,
-    private ConfigHelpers: IXosConfigHelpersService
+    private ConfigHelpers: IXosConfigHelpersService,
+    private ModelRest: IXosResourceService
   ) {
     this.data = this.$state.current.data;
     this.tableCfg = this.data.xosTableCfg;
@@ -39,19 +43,7 @@
 
     this.related = $state.current.data.related;
 
-    this.formCfg = {
-      formName: 'sampleForm',
-      actions: [
-        {
-          label: 'Save',
-          icon: 'ok', // refers to bootstraps glyphicon
-          cb: (item) => { // receive the model
-            console.log(item);
-          },
-          class: 'success'
-        }
-      ]
-    };
+    this.formCfg = $state.current.data.xosFormCfg;
 
     this.store.query(this.data.model)
       .subscribe(
@@ -61,8 +53,8 @@
             this.title = this.ConfigHelpers.pluralize(this.data.model, event.length);
             this.tableData = event;
 
-            // if it is a detail page
-            if ($stateParams['id']) {
+            // if it is a detail page for an existing model
+            if ($stateParams['id'] && $stateParams['id'] !== 'add') {
               this.model = _.find(this.tableData, {id: parseInt($stateParams['id'], 10)});
             }
           });
@@ -72,11 +64,19 @@
     // if it is a detail page
     if ($stateParams['id']) {
       this.list = false;
+
+      // if it is the create page
+      if ($stateParams['id'] === 'add') {
+        // generate a resource for an empty model
+        const endpoint = this.ConfigHelpers.urlFromCoreModel(this.data.model);
+        const resource = this.ModelRest.getResource(endpoint);
+        this.model = new resource({});
+      }
     }
   }
 
   public getRelatedItem(relation: string, item: any): number {
-    if (angular.isDefined(item[relation.toLowerCase()])) {
+    if (item && angular.isDefined(item[relation.toLowerCase()])) {
       return item[relation.toLowerCase()];
     }
     return 0;
@@ -88,3 +88,4 @@
   controllerAs: 'vm',
   controller: CrudController
 };
+