webstaff: new directive: egEditFmRecord
authorGalen Charlton <gmc@esilibrary.com>
Fri, 10 Jun 2016 22:14:20 +0000 (18:14 -0400)
committerMike Rylander <mrylander@gmail.com>
Thu, 18 Aug 2016 19:34:21 +0000 (15:34 -0400)
This implements a generic IDL record editor widget:

<eg-edit-fm-record
  idl-class            = "xyz"
  mode                 = "update"
  record-id            = "223"
  hidden-fields        = "bar,baz"
  readonly-fields      = "quux"
  required-fields      = "foo"
  is-required-override = "bundle_of_custom_functions"
  on-save              = "on_save_handler"
  on-cancel            = "on_cancel"
></eg-edit-fm-record>

The mode can be either "create" or "update"; if it is "create",
then it is not necessary or desired to pass a record-id.

Currently eg-edit-fm-record expects to be invoked from
inside a uibModal.

Signed-off-by: Galen Charlton <gmc@esilibrary.com>
Open-ILS/src/templates/staff/share/t_fm_record_editor.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/services/fm_record_editor.js [new file with mode: 0644]

diff --git a/Open-ILS/src/templates/staff/share/t_fm_record_editor.tt2 b/Open-ILS/src/templates/staff/share/t_fm_record_editor.tt2
new file mode 100644 (file)
index 0000000..18ee809
--- /dev/null
@@ -0,0 +1,62 @@
+<form role="form" class="form-validated eg-edit-fm-record">
+
+  <div class="modal-header">
+    <button type="button" class="close"
+      ng-click="cancel()" aria-hidden="true">&times;</button>
+    <h4 class="modal-title">{{record_label}}</h4>
+  </div>
+  <div class="modal-body">
+    <div class="form-group row" ng-repeat="field in fields | filter:{virtual:'!true'}">
+      <div class="col-md-3">
+        <label for="rec-{{field.name}}">{{field.label}}</label>
+      </div>
+      <div class="col-md-9">
+        <span  ng-if="field.datatype == 'id'">{{rec[field.name]()}}</span>
+        <input ng-if="field.datatype == 'text'"
+          ng-readonly="field.readonly"
+          ng-required="field.is_required()"
+          ng-model="rec[field.name]"
+          ng-model-options="{ getterSetter : true }">
+        </input>
+        <input ng-if="field.datatype == 'int'"
+          type="number"
+          ng-readonly="field.readonly"
+          ng-required="field.is_required()"
+          ng-model="rec[field.name]"
+          ng-model-options="{ getterSetter : true }">
+        </input>
+        <input ng-if="field.datatype == 'float'"
+          type="number" step="0.1"
+          ng-readonly="field.readonly"
+          ng-required="field.is_required()"
+          ng-model="rec[field.name]"
+          ng-model-options="{ getterSetter : true }">
+        </input>
+        <input ng-if="field.datatype == 'bool'"
+          type="checkbox"
+          ng-readonly="field.readonly"
+          ng-model="rec[field.name]"
+          ng-model-options="{ getterSetter : true }">
+        </input>
+        <span ng-if="field.datatype == 'link'"
+          ng-class="{nullable : !field.is_required()}">
+          <select ng-if="field.datatype == 'link'"
+            ng-readonly="field.readonly"
+            ng-required="field.is_required()"
+            ng-options="item.id as item.name for item in field.linked_values"
+            ng-model="rec[field.name]"
+            ng-model-options="{ getterSetter : true }">
+          </select>
+        </span>
+        <eg-org-selector ng-if="field.datatype == 'org_unit'"
+          selected="rec_orgs[field.name]()"
+          onchange="rec_orgs[field.name]">
+        </eg-org-selector>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-primary" ng-click="ok()">[% l('Save') %]</button>
+    <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</form>
diff --git a/Open-ILS/web/js/ui/default/staff/services/fm_record_editor.js b/Open-ILS/web/js/ui/default/staff/services/fm_record_editor.js
new file mode 100644 (file)
index 0000000..da95d54
--- /dev/null
@@ -0,0 +1,187 @@
+angular.module('egFmRecordEditorMod',
+    ['egCoreMod', 'egUiMod', 'ui.bootstrap'])
+
+.directive('egEditFmRecord', function() {
+    return {
+        restrict : 'AE',
+        transclude : true,
+        scope : {
+            // IDL class hint (e.g. "aou")
+            idlClass : '@',
+
+            // mode: 'create' for creating a new record,
+            //       'update' for editing an existing record
+            mode : '@',
+
+            // record ID to update
+            recordId : '=',
+
+            // comma-separated list of fields that should not be
+            // displayed
+            hiddenFields : '@',
+
+            // comma-separated list of fields that should always
+            // be read-only
+            readonlyFields : '@',
+
+            // comma-separated list of required fields; this
+            // supplements what the IDL considers required
+            requiredFields : '@',
+
+            // hash, keyed by field name, of functions to invoke
+            // to check whether a field is required.  Each
+            // callback is passed the field name and the record
+            // and should return a boolean value. This supports
+            // cases where whether a field is required or not
+            // depends on the current value of another field.
+            isRequiredOverride : '@',
+
+            // reference to handler to run upon saving
+            // record. The handler will be passed the
+            // record ID and a parameter indicating whether
+            // the save did a create or an update. Note that
+            // even if the mode of the egEditFmRecord is
+            // 'create', the onSave handler may still get
+            // 'update' if the user is permitted to create a
+            // record, then update it
+            onSave : '=',
+
+            // reference to handler to run if the user
+            // cancels the dialog
+            onCancel : '='
+
+        },
+
+        templateUrl : '/eg/staff/share/t_fm_record_editor',
+
+        controller : [
+                    '$scope','egCore',
+            function($scope , egCore) {
+
+            function list_to_hash(str) {
+                var hash = {};
+                if (angular.isString(str)) {
+                    str.split(/,/).map(function(s) {
+                        hash[s.trim()] = true;
+                    });
+                }
+                return hash;
+            }
+
+            $scope.required = list_to_hash($scope.requiredFields);
+            $scope.readonly = list_to_hash($scope.readonlyFields);
+            $scope.hidden = list_to_hash($scope.hiddenFields);
+
+            $scope.record_label = egCore.idl.classes[$scope.idlClass].label;
+            $scope.rec_orgs = {};
+
+            if ($scope.mode == 'update') {
+                egCore.pcrud.retrieve($scope.idlClass, $scope.recordId).then(function(r) {
+                    $scope.rec = r;
+                    convert_datatypes_to_js($scope.rec);
+                    $scope.fields = get_field_list();
+                });
+            } else {
+                $scope.rec = new egCore.idl[$scope.idlClass]();
+                $scope.fields = get_field_list();
+            }
+
+            function convert_datatypes_to_js(rec) {
+                var fields = egCore.idl.classes[$scope.idlClass].fields;
+                angular.forEach(fields, function(field) {
+                    if (field.datatype == 'bool') {
+                        if (rec[field.name]() == 't') {
+                            rec[field.name](true);
+                        } else if (rec[field.name]() == 'f') {
+                            rec[field.name](false);
+                        }
+                    }
+                });
+            }
+
+            function convert_datatypes_to_idl(rec) {
+                var fields = egCore.idl.classes[$scope.idlClass].fields;
+                angular.forEach(fields, function(field) {
+                    if (field.datatype == 'bool') {
+                        if (rec[field.name]() == true) {
+                            rec[field.name]('t');
+                        } else if (rec[field.name]() == false) {
+                            rec[field.name]('f');
+                        }
+                    }
+                });
+            }
+
+            function flatten_linked_values(cls, list) {
+                var results = [];
+                var fields = egCore.idl.classes[cls].fields;
+                var id_field;
+                var selector;
+                angular.forEach(fields, function(fld) {
+                    if (fld.datatype == 'id') {
+                        id_field = fld.name;
+                        selector = fld.selector ? fld.selector : id_field;
+                        return;
+                    }
+                });
+                angular.forEach(list, function(item) {
+                    var rec = egCore.idl.toHash(item);
+                    results.push({
+                        id : rec[id_field],
+                        name : rec[selector]
+                    });
+                });
+                return results;
+            }
+
+            function get_field_list() {
+                var fields = egCore.idl.classes[$scope.idlClass].fields;
+
+                angular.forEach(fields, function(field) {
+                    field.readonly = (field.name in $scope.readonly);
+                    if (angular.isObject($scope.isRequiredOverride) &&
+                        field.name in $scope.isRequiredOverride) {
+                        field.is_required = function() {
+                            return $scope.isRequiredOverride[field.name](field.name, $scope.rec);
+                        }
+                    } else {
+                        field.is_required = function() {
+                            return field.required || (field.name in $scope.required);
+                        }
+                    }
+                    if (field.datatype == 'link') {
+                    egCore.pcrud.retrieveAll(
+                            field.class, {}, {atomic : true}
+                        ).then(function(list) {
+                            field.linked_values = flatten_linked_values(field.class, list);
+                        });
+                    }
+                    if (field.datatype == 'org_unit') {
+                        $scope.rec_orgs[field.name] = function(org) {
+                            if (arguments.length == 1) $scope.rec[field.name](org.id());
+                            return egCore.org.get($scope.rec[field.name]());
+                        }
+                    }
+                });
+                return fields.filter(function(field) { return !(field.name in $scope.hidden) });
+            }
+
+            $scope.ok = function($event) {
+                var recToSave = egCore.idl.Clone($scope.rec)
+                convert_datatypes_to_idl(recToSave);
+                if ($scope.mode == 'update') {
+                    egCore.pcrud.update(recToSave).then(function() {
+                        $scope.onSave($event);
+                    });
+                } else {
+                    egCore.pcrud.create(recToSave).then(function() {
+                        $scope.onSave($event);
+                    });
+                }
+            }
+            $scope.cancel = function($event) {
+                $scope.onCancel($event);
+            }
+        }]
+    };
+})