LPXXX Angular Volcopy
authorBill Erickson <berickxx@gmail.com>
Wed, 24 Jun 2020 19:23:27 +0000 (15:23 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 24 Jun 2020 19:23:27 +0000 (15:23 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts

index 2f5811b..5128eda 100644 (file)
   <div class="col-lg-7 d-flex">
     <button class="btn btn-outline-dark mr-2" (click)="applyTemplate()" i18n>Apply</button>
     <button class="btn btn-outline-dark mr-2" (click)="saveTemplate()" i18n>Save</button>
+    <!--
     <button class="btn btn-outline-dark mr-2" (click)="importTemplate()" i18n>Import</button>
+    -->
 
-    <button class="btn btn-outline-dark mr-2" (click)="importTemplate()" i18n>Import</button>
+    <!-- 
+      The type typical approach of wrapping a file input in a <label>
+      results in button-ish things that have slightly different dimensions
+    -->
+    <button class="btn btn-outline-dark mr-2" (click)="templateFile.click()">
+      <input type="file" class="d-none" #templateFile
+        (change)="importTemplate($event)" id="template-file-upload"/>
+      <span i18n>Import</span>
+    </button>
 
     <a (click)="exportTemplate($event)"
       download="export_copy_template.json" [href]="exportTemplateUrl()">
index 2eb0ef2..dff2033 100644 (file)
@@ -303,7 +303,7 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
 
         this.store.setLocalItem('cat.copy.last_template', entry.id);
 
-        // TODO: handle owning_lib and statcats differently.
+        // TODO: handle owning_lib and statcats differently, location
 
         const template = this.volcopy.templates[entry.id];
 
@@ -312,12 +312,27 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
 
             if (value === null || value === undefined) { return; }
 
-            this.applyCopyValue(field, value);
+            // In some cases, we may have to fetch the data since
+            // the local code assumes copy field is fleshed.
+            let promise = Promise.resolve(value);
 
-            // Indicate in the form these values have changed
-            this.batchAttrs
-                .filter(ba => ba.name === field)
-                .forEach(attr => attr.hasChanged = true);
+            // TODO: promises in loops are dangerous becuase they
+            // can lead to blasts of duplicate requests for non-local
+            // data.  Consider an alternative approach.
+
+            if (field === 'location') {
+                // May be a 'remote' location.  Fetch as needed.
+                promise = this.volcopy.getLocation(value);
+            }
+
+            promise.then(val => {
+                this.applyCopyValue(field, val);
+
+                // Indicate in the form these values have changed
+                this.batchAttrs
+                    .filter(ba => ba.name === field)
+                    .forEach(attr => attr.hasChanged = true);
+            });
         });
     }
 
@@ -330,6 +345,8 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
 
         if (entry.freetext) {
             name = entry.label;
+            // freetext entries don't have an ID, but we may need one later.
+            entry.id = entry.label;
             template = {};
         } else {
             name = entry.id;
@@ -344,14 +361,17 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
                 const value = copy[comp.name]();
                 if (value === null) {
                     delete template[comp.name];
+
                 } else {
-                    template[comp.name] = value;
+                    // some values are fleshed.
+                    // this assumes fleshed objects have an 'id' value,
+                    // which is true so far.
+                    template[comp.name] =
+                        typeof value === 'object' ?  value.id() : value;
                 }
             }
         });
 
-        console.debug('Saving template', template);
-
         this.volcopy.templates[name] = template;
         this.volcopy.saveTemplates();
     }
@@ -359,13 +379,33 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
     exportTemplate($event) {
         if (this.fileExport.inProgress()) { return; }
 
-        const entry: ComboboxEntry = this.copyTemplateCbox.selected;
-        if (!entry) { return; }
+        this.fileExport.exportFile(
+            $event, JSON.stringify(this.volcopy.templates), 'text/json');
+    }
 
-        const template = this.volcopy.templates[entry.id];
+    importTemplate($event) {
+        const file: File = $event.target.files[0];
+        if (!file) { return; }
 
-        this.fileExport.exportFile(
-            $event, JSON.stringify(template), 'text/json');
+        const reader = new FileReader();
+
+        reader.addEventListener('load', () => {
+
+            try {
+                const template = JSON.parse(reader.result as string);
+                const name = Object.keys(template)[0];
+                this.volcopy.templates[name] = template[name];
+            } catch (E) {
+                console.error('Invalid Item Attribute template', E);
+                return;
+            }
+
+            this.volcopy.saveTemplates();
+            // Adds the new one to the list and re-sorts the labels.
+            this.volcopy.fetchTemplates();
+        });
+
+        reader.readAsText(file);
     }
 
     // Returns null when no export is in progress.
@@ -373,6 +413,14 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         return this.fileExport.safeUrl;
     }
 
+    deleteTemplate() {
+        const entry: ComboboxEntry = this.copyTemplateCbox.selected;
+        if (!entry) { return; }
+        delete this.volcopy.templates[entry.id];
+        this.volcopy.saveTemplates();
+        this.copyTemplateCbox.selected = null;
+    }
+
     displayAttr(field: string): boolean {
         return this.volcopy.defaults.hidden[field] !== true;
     }
index b411fd1..63081e3 100644 (file)
@@ -27,10 +27,15 @@ export class VolCopyService {
 
     autoId = -1;
 
+    localOrgs: number[];
     defaults: VolCopyDefaults = null;
     defaultLocation: IdlObject;
     copyStatuses: {[id: number]: IdlObject} = null;
 
+    // This will be all 'local' copy locations plus any remote
+    // locations that we are required to interact with.
+    copyLocationMap: {[id: number]: IdlObject} = {};
+
     // Track this here so it can survive route changes.
     currentContext: VolCopyContext;
 
@@ -65,9 +70,10 @@ export class VolCopyService {
 
         if (this.itemTypeMaps.length > 0) { return Promise.resolve(); }
 
-        const myOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
+        this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
 
         return this.fetchDefaults()
+        .then(_ => this.getLocations())
         .then(_ => this.holdings.fetchCallNumberClasses())
         .then(cls => this.volClasses = cls)
 
@@ -75,7 +81,7 @@ export class VolCopyService {
             return this.holdings.fetchCallNumberPrefixes().then(prefixes =>
                 this.volPrefixes = prefixes
                     .filter(sfx => sfx.id() !== -1)
-                    .filter(pfx => myOrgs.includes(pfx.owning_lib()))
+                    .filter(pfx => this.localOrgs.includes(pfx.owning_lib()))
             );
         })
 
@@ -83,7 +89,7 @@ export class VolCopyService {
             return this.holdings.fetchCallNumberSuffixes().then(suffixes =>
                 this.volSuffixes = suffixes
                     .filter(sfx => sfx.id() !== -1)
-                    .filter(sfx => myOrgs.includes(sfx.owning_lib()))
+                    .filter(sfx => this.localOrgs.includes(sfx.owning_lib()))
             );
         })
 
@@ -159,6 +165,26 @@ export class VolCopyService {
         });
     }
 
+    getLocation(id: number): Promise<IdlObject> {
+        if (this.copyLocationMap[id]) {
+            return Promise.resolve(this.copyLocationMap[id]);
+        }
+
+        return this.pcrud.retrieve('acpl', id)
+            .pipe(tap(loc => this.copyLocationMap[loc.id()] = loc))
+            .toPromise();
+    }
+
+    getLocations(): Promise<any> {
+        this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
+
+        return this.pcrud.search('acpl',
+            {deleted: 'f', owning_lib: this.localOrgs},
+            {order_by: {acpl: 'name'}}
+        ).pipe(tap(loc => this.copyLocationMap[loc.id()] = loc)
+        ).toPromise();
+    }
+
     fetchTemplates(): Promise<any> {
 
         // TODO: copy templates should be server settings
@@ -194,9 +220,9 @@ export class VolCopyService {
             // Use the first non-deleted copy location within org unit
             // range as the default.  Typically "Stacks".
 
-            const myOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
+            this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
             return this.pcrud.search('acpl',
-                {deleted: 'f', owning_lib: myOrgs},
+                {deleted: 'f', owning_lib: this.localOrgs},
                 {order_by: {acpl: 'id'}, limit: 1}
             ).toPromise().then(loc => this.defaultLocation = loc);
         });