LP#1626157 fm editor xport experiment
authorBill Erickson <berickxx@gmail.com>
Mon, 16 Apr 2018 19:10:39 +0000 (19:10 +0000)
committerBill Erickson <berickxx@gmail.com>
Mon, 16 Apr 2018 19:10:39 +0000 (19:10 +0000)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/accesskey/accesskey-info.component.html
Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/org-select.component.html
Open-ILS/src/eg2/src/app/share/org-select.component.ts
Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.component.html
Open-ILS/src/eg2/src/app/staff/admin/workstation/workstations/workstations.module.ts
Open-ILS/src/eg2/src/styles.css

index cbb0088..82ed72a 100644 (file)
@@ -21,6 +21,6 @@
   </div>
   <div class="modal-footer">
     <button type="button" class="btn btn-success" 
-      (click)="close(promptValue)" i18n>Close</button>
+      (click)="close()" i18n>Close</button>
   </div>
 </ng-template>
index 4da4f0a..0b2cbbc 100644 (file)
@@ -1,5 +1,5 @@
 import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
-import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
+import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
 
 /** 
  * Dialog base class.  Handles the ngbModal logic. 
@@ -25,14 +25,14 @@ export class EgDialogComponent {
 
     constructor(private modalService: NgbModal) {}
 
-    open(): Promise<any> {
+    open(options?: NgbModalOptions): Promise<any> {
 
         if (this.modalRef !== null) {
             console.warn('Dismissing existing dialog');
             this.dismiss();
         }
 
-        this.modalRef = this.modalService.open(this.dialogContent);
+        this.modalRef = this.modalService.open(this.dialogContent, options);
         return new Promise( (resolve, reject) => {
 
             this.modalRef.result.then(
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.html
new file mode 100644 (file)
index 0000000..c49da97
--- /dev/null
@@ -0,0 +1,44 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title" i18n>Record Editor</h4>
+    <button type="button" class="close" 
+      i18n-aria-label aria-label="Close" 
+      (click)="dismiss('cross_click')">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+    <form role="form">
+      <div class="form-group row" *ngFor="let field of fields">
+        <div class="col-lg-3">
+          <label for="rec-{{field.name}}">{{field.label}}</label>
+        </div>
+        <div class="col-lg-9">
+          <!-- TODO custom field templates -->
+          <span *ngIf="field.datatype == 'id' && !pkeyIsEditable">
+            {{record[field.name]()}}
+          </span>
+
+          <input *ngIf="field.datatype == 'id' && pkeyIsEditable"
+            name="{{field.name}}"
+            [readonly]="field.readOnly"
+            [required]="field.isRequired()"
+            [ngModel]="record[field.name]()"
+            (ngModelChange)="record[field.name]($event)"/>
+
+          <input *ngIf="field.datatype == 'text'"
+            name="{{field.name}}"
+            [readonly]="field.readOnly"
+            [required]="field.isRequired()"
+            [ngModel]="record[field.name]()"
+            (ngModelChange)="record[field.name]($event)"/>
+
+        </div>
+      </div>
+    </form>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" 
+      (click)="close()" i18n>Close</button>
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts b/Open-ILS/src/eg2/src/app/share/fm-editor/fm-editor.component.ts
new file mode 100644 (file)
index 0000000..b2fbe6d
--- /dev/null
@@ -0,0 +1,174 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {EgStoreService} from '@eg/core/store.service';
+import {EgOrgService} from '@eg/core/org.service';
+import {EgIdlService, EgIdlObject} from '@eg/core/idl.service';
+import {EgPcrudService} from '@eg/core/pcrud.service';
+import {EgDialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+  selector: 'fm-record-editor',
+  templateUrl: './fm-editor.component.html'
+})
+export class FmRecordEditorComponent 
+    extends EgDialogComponent implements OnInit {
+
+    // IDL class hint (e.g. "aou")
+    @Input() idlClass: string;
+
+    // mode: 'create' for creating a new record,
+    //       'update' for editing an existing record
+    @Input() mode: string;
+
+    // record ID to update.  Not all IDs are numbers.
+    @Input() recordId: string; 
+
+    // TODO
+    // customFieldTemplates
+
+    // list of fields that should not be displayed
+    @Input() hiddenFields: string[] = [];
+
+    // list of fields that should always be read-only
+    @Input() readonlyFields: string[] = [];
+
+    // list of required fields; this supplements what the IDL considers
+    // required
+    @Input() requiredFields: string[] = [];
+
+    // list of org_unit fields where the selector should default to the
+    // workstation OU
+    @Input() orgDefaultAllowed: string[] = [];
+
+    // 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.
+    @Input() isRequiredOverride: 
+        {[field: string] : (field: string, record: EgIdlObject) => boolean};
+
+    // IDL record display label.  Defaults to the IDL label.
+    @Input() recordLabel: string;
+
+    // Emit the modified object when the save action completes.
+    @Output() onSave$ = new EventEmitter<EgIdlObject>();
+
+    // Emit the original object when the save action is canceled.
+    @Output() onCancel$ = new EventEmitter<EgIdlObject>();
+
+    // Emit an error message when the save action fails.
+    @Output() onError$ = new EventEmitter<string>();
+
+    // IDL info for the the selected IDL class
+    idlDef: any;
+
+    // IDL record we are editing
+    record: EgIdlObject;
+
+    // Can we edit the primary key?
+    pkeyIsEditable: boolean = false;
+
+    // List of IDL field definitions.  This is a subset of the full
+    // list of fields on the IDL, since some are hidden, virtual, etc.
+    fields: any[];
+
+    constructor(
+      private modal: NgbModal, // required for passing to parent
+      private idl: EgIdlService,
+      private store: EgStoreService,
+      private org: EgOrgService,
+      private pcrud: EgPcrudService
+    ) { 
+      super(modal) 
+    }
+    
+    ngOnInit() {
+        this.idlDef = this.idl.classes[this.idlClass];
+        this.recordLabel = this.idlDef.label;
+        this.initRecord();
+    }
+
+    private initRecord(): Promise<any> {
+
+        if (this.mode == 'update') {
+            return this.pcrud.retrieve(this.idlClass, this.recordId)
+            .toPromise().then(rec => {
+                this.record = rec;
+                this.convertDatatypesToJs();
+                this.fields = this.getFieldList();
+            });
+        } 
+
+        // create a new record from scratch
+        this.pkeyIsEditable = !('pkey_sequence' in this.idlDef);
+        this.record = this.idl.create(this.idlClass);
+        this.fields = this.getFieldList();
+        return Promise.resolve();
+    }
+
+    // Modifies the FM record in place, replacing IDL-compatible values
+    // with native JS values.
+    private convertDatatypesToJs() {
+        this.idlDef.fields.forEach(field => {
+            if (field.datatype == 'bool') {
+                if (this.record[field.name]() == 't') {
+                    this.record[field.name](true);
+                } else if (this.record[field.name]() == 'f') {
+                    this.record[field.name](false);
+                }
+            }
+        });
+    }
+
+    private flattenLinkedValues(cls: string, list: EgIdlObject[]): any[] {
+        let results: any[] = [];
+        let idField: string = this.idlDef.pkey;
+        let selector: string = 
+            this.idlDef.field_map[idField].selector || idField;
+
+        return list.map(item => {
+            return {id: item[idField](), name: item[selector]()}
+        });
+    }
+
+    private getFieldList(): any[] {
+
+        let fields = this.idlDef.fields.filter(f => 
+            f.virtual != 'true' && 
+            !this.hiddenFields.includes(f.name)
+        );
+
+        fields.forEach(field => {
+            field.readOnly = this.readonlyFields.includes(field.name);
+
+            if (this.isRequiredOverride && 
+                field.name in this.isRequiredOverride) {
+                field.isRequired = () => {
+                    return this.isRequiredOverride[field.name](field.name, this.record);
+                }
+            } else {
+                field.isRequired = () => {
+                    return field.required || 
+                        this.requiredFields.includes(field.name);
+                }
+            }
+
+            // TODO
+        });
+
+        console.debug(fields);
+        return fields;
+    }
+
+    save() {
+        // save changes here
+        this.close(/* data for caller */);
+    }
+
+    cancel() {
+        this.dismiss(/* data for caller */);
+    }
+}
+
+
index e628ab2..2a4bd3a 100644 (file)
@@ -5,7 +5,7 @@
 </ng-template>
 
 <input type="text" 
-  class="form-control" 
+  class="form-control"
   [placeholder]="placeholder"
   [(ngModel)]="selected" 
   [ngbTypeahead]="filter"
index 4d0e776..855eee0 100644 (file)
@@ -29,7 +29,6 @@ export class EgOrgSelectComponent implements OnInit {
     startOrg: EgIdlObject;
     hidden: number[] = [];
     disabled: number[] = [];
-    //focus$ = new Subject<string>();
     click$ = new Subject<string>();
 
     @ViewChild('instance') instance: NgbTypeahead;
@@ -86,7 +85,7 @@ export class EgOrgSelectComponent implements OnInit {
 
     filter = (text$: Observable<string>): Observable<OrgDisplay[]> => {
         return text$
-            .debounceTime(100)
+            .debounceTime(200)
             .distinctUntilChanged()
             .merge(this.click$.filter(() => !this.instance.isPopupOpen()))
             .map(term => {
index cf5d89b..32a1449 100644 (file)
@@ -9,6 +9,11 @@
   dialogBody='Workstation "{{newName}}" already exists.  Use it anyway?'>
 </eg-confirm-dialog>
 
+<!-- TESTING TESTING -->
+<fm-record-editor #fmRecordEditor idlClass="cbt" mode="update" recordId="1">
+</fm-record-editor>
+<button (click)="fmRecordEditor.open({size:'lg'})">Fm Record Editor Test</button>
+
 <div class="row">
   <div class="col-lg-8 offset-1 mt-3">
     <div class="alert alert-warning" *ngIf="removeWorkstation" i18n>
index 064b24d..9692435 100644 (file)
@@ -3,9 +3,13 @@ import {EgStaffCommonModule} from '@eg/staff/common.module';
 import {WorkstationsRoutingModule} from './routing.module';
 import {WorkstationsComponent} from './workstations.component';
 
+// XXX testing
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+
 @NgModule({
   declarations: [
     WorkstationsComponent,
+    FmRecordEditorComponent // XXX
   ],
   imports: [
     EgStaffCommonModule,
index 9f7b83a..835d80d 100644 (file)
@@ -40,7 +40,7 @@ h5 {font-size: .95rem}
 .flex-5 {flex: 5}
 
 
-/* usefulf for mat-icon buttons without any background or borders */
+/* usefuf for mat-icon buttons without any background or borders */
 .material-icon-button {
   /* Transparent background */
   border: none;
@@ -67,3 +67,14 @@ h5 {font-size: .95rem}
   padding: .25rem;
 }
 
+@media all and (min-width: 800px) {                                            
+    /* scrollable typeahead menus for full-size screens */                               
+    ngb-typeahead-window {
+        height: auto;                                                          
+        max-height: 200px;                                                     
+        overflow-x: hidden;                                                    
+    }
+}
+
+
+