LPXXX Angular Volcopy / TABS
authorBill Erickson <berickxx@gmail.com>
Fri, 19 Jun 2020 16:15:57 +0000 (12:15 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 19 Jun 2020 16:15:57 +0000 (12:15 -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/routing.module.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.css
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts
Open-ILS/src/eg2/src/styles.css

index ca2e2e2..789f657 100644 (file)
 
 
 <!-- Copy Templates -->
-<div class="row">
+<div class="row border rounded border-dark pt-2 pb-2 bg-faint">
+  <div class="col-lg-1 font-weight-bold" i18n>Templates:</div>
+  <div class="col-lg-4">
+    <eg-combobox domId="template-select" #copyTemplateCbox></eg-combobox>
+  </div>
+  <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)="exportTemplate()" i18n>Export</button>
+    <div class="flex-1"> </div>
+    <button class="btn btn-outline-dark mr-2" 
+      (click)="copyTemplateCbox.selectedId = null" i18n>Clear</button>
+    <button class="btn btn-outline-danger mr-2" (click)="deleteTemplate()" i18n>Delete Template</button>
+  </div>
 </div>
 
 
   <div class="flex-1 p-1">
     <div class="p-1"><h4 class="font-weight-bold" i18n>Statistics</h4></div>
 
-    <div *ngFor="let cat of statCats; let idx = index">
+    <div *ngFor="let cat of statCats(); let idx = index">
       <ng-template #statCatTemplate>
         <eg-combobox domId="stat-cat-input-{{idx}}"
           (ngModelChange)="statCatValues[cat.id()] = $event ? $event.id : null"
index ffc40f6..db3bcda 100644 (file)
@@ -4,6 +4,7 @@ import {tap} from 'rxjs/operators';
 import {IdlObject, IdlService} from '@eg/core/idl.service';
 import {EventService} from '@eg/core/event.service';
 import {OrgService} from '@eg/core/org.service';
+import {StoreService} from '@eg/core/store.service';
 import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
 import {PcrudService} from '@eg/core/pcrud.service';
@@ -14,14 +15,16 @@ import {FormatService} from '@eg/core/format.service';
 import {StringComponent} from '@eg/share/string/string.component';
 import {CopyAlertsDialogComponent
     } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
 
 @Component({
   selector: 'eg-copy-attrs',
   templateUrl: 'copy-attrs.component.html',
 
   // Match the header of the batch attrs component
-  styles: [`
-    .batch-header {background-color: #d9edf7;}`
+  styles: [
+    `.batch-header {background-color: #EBF4FA;}`,
+    `.template-row {background-color: #EBF4FA;}`
   ]
 })
 export class CopyAttrsComponent implements OnInit, AfterViewInit {
@@ -35,13 +38,6 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
     // Map of stat ID to entry ID.
     statCatValues: {[statId: number]: number} = {};
 
-    ageProtectRules: IdlObject[] = [];
-    floatingGroups: IdlObject[] = [];
-    itemTypeMaps: IdlObject[] = [];
-    circModifiers: IdlObject[] = [];
-    statCats: IdlObject[] = [];
-    statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
-
     loanDurationLabelMap: {[level: number]: string} = {};
     fineLevelLabelMap: {[level: number]: string} = {};
 
@@ -67,6 +63,9 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
     @ViewChild('copyAlertsDialog', {static: false})
         private copyAlertsDialog: CopyAlertsDialogComponent;
 
+    @ViewChild('copyTemplateCbox', {static: false})
+        copyTemplateCbox: ComboboxComponent;
+
     constructor(
         private router: Router,
         private route: ActivatedRoute,
@@ -79,15 +78,18 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         private pcrud: PcrudService,
         private holdings: HoldingsService,
         private volcopy: VolCopyService,
-        private format: FormatService
+        private format: FormatService,
+        private store: StoreService
     ) { }
 
     ngOnInit() {
-        this.load();
     }
 
     ngAfterViewInit() {
 
+        const tmpl = this.store.getLocalItem('cat.copy.last_template');
+        if (tmpl) { this.copyTemplateCbox.selectedId = tmpl; }
+
         this.loanDurationLabelMap[1] = this.loanDurationShort.text;
         this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
         this.loanDurationLabelMap[3] = this.loanDurationLong.text;
@@ -97,75 +99,11 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
     }
 
-    load() {
-
-        this.pcrud.retrieveAll('crahp')
-        .pipe(tap(rule => this.ageProtectRules.push(rule))).toPromise()
-        .then(_ => {
-
-            this.ageProtectRules = this.ageProtectRules.sort(
-                (a, b) => a.name() < b.name() ? -1 : 1);
-
-        }).then(_ => {
-
-            return this.pcrud.retrieveAll('cfg')
-            .pipe(tap(rule => this.floatingGroups.push(rule))).toPromise();
-
-        }).then(_ => {
-
-            this.floatingGroups = this.floatingGroups.sort(
-                (a, b) => a.name() < b.name() ? -1 : 1);
-
-        }).then(_ => {
-
-            return this.pcrud.retrieveAll('ccm')
-            .pipe(tap(rule => this.circModifiers.push(rule))).toPromise();
-
-        }).then(_ => {
-
-            this.circModifiers = this.circModifiers.sort(
-                (a, b) => a.name() < b.name() ? -1 : 1);
-
-        }).then(_ => {
-
-            return this.pcrud.retrieveAll('citm')
-            .pipe(tap(itemType => this.itemTypeMaps.push(itemType))).toPromise();
-
-        }).then(_ => {
-
-            this.itemTypeMaps = this.itemTypeMaps.sort(
-                (a, b) => a.value() < b.value() ? -1 : 1);
-
-        }).then(_ => {
-
-            return this.net.request('open-ils.circ',
-                'open-ils.circ.stat_cat.asset.retrieve.all',
-                this.auth.token(), this.auth.user().ws_ou()
-            ).toPromise().then(stats => this.statCats = stats);
-
-        }).then(_ => {
-
-            // Sort most local to the front of the list.
-            this.statCats = this.statCats.sort((s1, s2) => {
-                const d1 = this.org.get(s1.owner()).ou_type().depth();
-                const d2 = this.org.get(s2.owner()).ou_type().depth();
-
-                if (d1 > d2) {
-                    return -1;
-                } else if (d1 < d2) {
-                    return 1;
-                } else {
-                    return s1.name() < s2.name() ? -1 : 1;
-                }
-            });
-
-            this.statCats.forEach(cat => {
-                cat.entries().forEach(
-                    entry => this.statCatEntryMap[entry.id()] = entry);
-            });
-        });
+    statCats(): IdlObject[] {
+        return this.volcopy.statCats;
     }
 
+
     orgSn(orgId: number): string {
         return orgId ? this.org.get(orgId).shortname() : '';
     }
@@ -180,8 +118,8 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
 
             let value = '';
             if (entry) {
-                if (this.statCatEntryMap[entry.id()]) {
-                    value = this.statCatEntryMap[entry.id()].value();
+                if (this.volcopy.statCatEntryMap[entry.id()]) {
+                    value = this.volcopy.statCatEntryMap[entry.id()].value();
                 } else {
                     // Map to a remote stat cat.  Ignore.
                     return;
@@ -248,12 +186,12 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
                 return this.org.get(value).shortname();
 
             case 'age_protect':
-                const rule = this.ageProtectRules.filter(
+                const rule = this.volcopy.ageProtectRules.filter(
                     r => r.id() === Number(value))[0];
                 return rule ? rule.name() : '';
 
             case 'floating':
-                const grp = this.floatingGroups.filter(
+                const grp = this.volcopy.floatingGroups.filter(
                     g => g.id() === Number(value))[0];
                 return grp ? grp.name() : '';
 
@@ -264,12 +202,12 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
                 return this.fineLevelLabelMap[value];
 
             case 'circ_as_type':
-                const map = this.itemTypeMaps.filter(
+                const map = this.volcopy.itemTypeMaps.filter(
                     m => m.code() === value)[0];
                 return map ? map.value() : '';
 
             case 'circ_modifier':
-                const mod = this.circModifiers.filter(
+                const mod = this.volcopy.circModifiers.filter(
                     m => m.code() === value)[0];
                 return mod ? mod.name() : '';
 
@@ -332,7 +270,7 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
             }
 
             entry.id(entryId);
-            entry.value(this.statCatEntryMap[entryId].value());
+            entry.value(this.volcopy.statCatEntryMap[entryId].value());
 
             copy.ischanged(true);
         });
@@ -350,6 +288,23 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
             }
         );
     }
+
+    applyTemplate() {
+        const entry = this.copyTemplateCbox.selected;
+        if (!entry) { return; }
+
+        this.store.setLocalItem('cat.copy.last_template', entry.id);
+
+        const template = this.volcopy.templates[entry.id];
+
+        Object.keys(template).forEach(field => {
+            const value = template[field];
+
+            if (value === null || value === undefined) { return; }
+
+            this.applyCopyValue(field, value);
+        });
+    }
 }
 
 
index fb4f44d..c5d3f67 100644 (file)
@@ -3,16 +3,7 @@ import {RouterModule, Routes} from '@angular/router';
 import {VolCopyComponent} from './volcopy.component';
 
 const routes: Routes = [{
-    path: 'edit/item/:copy_id',
-    component: VolCopyComponent
-  }, {
-    path: 'edit/callnumber/:vol_id',
-    component: VolCopyComponent
-  }, {
-    path: 'edit/record/:record_id',
-    component: VolCopyComponent
-  }, {
-    path: 'edit/session/:session',
+    path: ':tab/:target/:target_id',
     component: VolCopyComponent
   /*
   }, {
index 96a852a..de37f46 100644 (file)
@@ -5,17 +5,11 @@ input[type="number"] {
 }
 
 .vol-row {
-  /*background-color: #d9edf7;*/
   background-color: rgba(0,0,0,.03);
   border-top: 1px solid #d9edf7;
   border-bottom: 1px solid #d9edf7;
 }
 
-.batch-vol-row {
-  border: 2px solid #d9edf7;
-}
-
-
 .clear-button {
   border: none;
   background-color: rgba(0, 0, 0, 0.0);
index b132b0a..f064447 100644 (file)
@@ -12,7 +12,7 @@
   dialogBody="Delete {{deleteCopyCount}} Item(s)?">
 </eg-confirm-dialog>
 
-<div class="row d-flex vol-row batch-vol-row mb-2">
+<div class="row d-flex bg-faint mb-2 border border-info rounded">
   <div class="p-1" [ngStyle]="{flex: flexAt(1)}"> </div>
   <div class="p-1" [ngStyle]="{flex: flexAt(2)}"> </div>
   <div class="p-1" [ngStyle]="{flex: flexAt(3)}">
index 156540f..af4ebe3 100644 (file)
@@ -1,11 +1,5 @@
 <eg-staff-banner bannerText="Holdings Editor" i18n-bannerText></eg-staff-banner>
 
-<div class="row" [hidden]="!loading">
-  <div class="col-lg-6 offset-lg-3">
-    <eg-progress-inline #loadingProgress></eg-progress-inline>
-  </div>
-</div>
-
 <div class="row" *ngIf="sessionExpired">
   <div class="col-lg-6 mt-4 offset-lg-3 alert alert-danger d-flex justify-content-center" i18n>
     Holdings Editor Session Expired
 <ng-container *ngIf="!loading && !sessionExpired">
 
   <eg-bib-summary *ngIf="context.recordId" [recordId]="context.recordId"></eg-bib-summary>
-  
-  <div class="mt-3" *ngIf="!context.hideVols">
-    <eg-vol-edit [context]="context"></eg-vol-edit>
-  </div>
-
-  <ng-container *ngIf="!context.hideVols && !context.hideCopies">
-    <hr class="m-2"/>
-  </ng-container>
-
-  <div class="mt-3" *ngIf="!context.hideCopies">
-    <eg-copy-attrs [context]="context"></eg-copy-attrs>
-  </div>
 
-  <div class="row m-2 p-2 border border-info">
-    <div class="col-lg-12 d-flex">
-      <div class="flex-1"> </div>
-      <button class="btn btn-outline-dark" 
-        [disabled]="!context.isSaveable()" (click)="save()" i18n>Save</button>
-      <button class="btn btn-outline-dark ml-2" 
-        [disabled]="!context.isSaveable()"
-        (click)="save(true)" i18n>Save &amp; Exit</button>
+  <div class="m-2"> </div>
+
+  <ngb-tabset [activeId]="tab" (tabChange)="beforeTabChange($event)">
+
+    <ngb-tab title="Holdings" i18n-title id="holdings">
+      <ng-template ngbTabContent>
+        <div class="mt-2">
+          <div class="row" [hidden]="!loading">
+            <div class="col-lg-6 offset-lg-3">
+              <eg-progress-inline #loadingProgress></eg-progress-inline>
+            </div>
+          </div>
+          <eg-vol-edit [context]="context"></eg-vol-edit>
+        </div>
+      </ng-template>
+    </ngb-tab>
+
+    <ngb-tab title="Item Attributes" i18n-title id="attrs">
+      <ng-template ngbTabContent>
+        <div class="mt-2">
+          <div class="row" [hidden]="!loading">
+            <div class="col-lg-6 offset-lg-3">
+              <eg-progress-inline #loadingProgress></eg-progress-inline>
+            </div>
+          </div>
+        </div>
+        <eg-copy-attrs [context]="context"></eg-copy-attrs>
+      </ng-template>
+    </ngb-tab>
+
+  </ngb-tabset>
+
+  <ng-container *ngIf="tab === 'holdings' || tab === 'attrs'">
+    <hr class="m-2"/>                                                          
+    <div class="row m-2 p-2 border border-info bg-faint">
+      <div class="col-lg-12 d-flex">
+        <div class="flex-1"> </div>
+        <button class="btn btn-outline-dark" 
+          [disabled]="!context.isSaveable()" (click)="save()" i18n>Save</button>
+        <button class="btn btn-outline-dark ml-2" 
+          [disabled]="!context.isSaveable()"
+          (click)="save(true)" i18n>Save &amp; Exit</button>
+      </div>
     </div>
-  </div>
+  </ng-container>
 
 </ng-container>
 
index 7d493f1..b737779 100644 (file)
@@ -12,6 +12,7 @@ import {VolCopyContext} from './volcopy';
 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
 import {VolCopyService} from './volcopy.service';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
 
 const COPY_FLESH = {
     flesh: 1,
@@ -48,6 +49,10 @@ export class VolCopyComponent implements OnInit {
     loading = true;
     sessionExpired = false;
 
+    tab = 'holdings'; // holdings | attrs | config
+    target: string;   // item | callnumber | record | session
+    targetId: string; // id value or session string
+
     @ViewChild('loadingProgress', {static: false})
     loadingProgress: ProgressInlineComponent;
 
@@ -67,35 +72,54 @@ export class VolCopyComponent implements OnInit {
     ) { }
 
     ngOnInit() {
-        this.context = new VolCopyContext();
-        this.context.org = this.org; // inject;
-
         this.route.paramMap.subscribe(
             (params: ParamMap) => this.negotiateRoute(params));
     }
 
     negotiateRoute(params: ParamMap) {
-        this.context.recordId = +params.get('record_id') || null;
-        this.context.volId    = +params.get('vol_id')    || null;
-        this.context.copyId   = +params.get('copy_id')   || null;
-        this.context.session  =  params.get('session')   || null;
-        this.load();
+        this.tab = params.get('tab') || 'holdings';
+        this.target = params.get('target');
+        this.targetId = params.get('target_id');
+
+        if (this.volcopy.currentContext) {
+            // Avoid clobbering the context on route change.
+            this.context = this.volcopy.currentContext;
+        } else {
+            this.context = new VolCopyContext();
+            this.context.org = this.org; // inject;
+        }
+
+        switch (this.target) {
+            case 'item':
+                this.context.copyId = +this.targetId;
+                break;
+            case 'callnumber':
+                this.context.volId = +this.targetId;
+                break;
+            case 'record':
+                this.context.recordId = +this.targetId;
+                break;
+            case 'session':
+                this.context.session = this.targetId;
+                break;
+        }
+
+        if (!this.volcopy.currentContext) {
+            // Avoid refetching the data during route changes.
+            this.volcopy.currentContext = this.context;
+            this.load();
+        }
     }
 
     load(copyIds?: number[]) {
-
         this.sessionExpired = false;
         this.loading = true;
         this.context.reset();
 
-        this.volcopy.fetchDefaults()
-        .then(_ => this.volcopy.fetchCopyStats())
+        this.volcopy.load()
         .then(_ => this.fetchHoldings(copyIds))
         .then(_ => this.volcopy.applyVolLabels(
             this.context.volNodes().map(n => n.target)))
-        .then(_ => this.holdings.fetchCallNumberClasses())
-        .then(_ => this.holdings.fetchCallNumberPrefixes())
-        .then(_ => this.holdings.fetchCallNumberSuffixes())
         .then(_ => this.context.sortHoldings())
         .then(_ => this.context.setRecordId())
         .then(_ => this.loading = false);
@@ -111,20 +135,36 @@ export class VolCopyComponent implements OnInit {
             this.context.sessionType = 'mixed';
             return this.fetchSession(this.context.session);
 
-        } else if (this.context.recordId) {
-            this.context.sessionType = 'record';
-            return this.fetchRecords(this.context.recordId);
+        } else if (this.context.copyId) {
+            this.context.sessionType = 'copy';
+            return this.fetchCopies(this.context.copyId);
 
         } else if (this.context.volId) {
             this.context.sessionType = 'vol';
             return this.fetchVols(this.context.volId);
 
-        } else if (this.context.copyId) {
-            this.context.sessionType = 'copy';
-            return this.fetchCopies(this.context.copyId);
+        } else if (this.context.recordId) {
+            this.context.sessionType = 'record';
+            return this.fetchRecords(this.context.recordId);
         }
     }
 
+    // Changing a tab in the UI means changing the route.
+    // Changing the route ultimately results in changing the tab.
+    beforeTabChange(evt: NgbTabChangeEvent) {
+        evt.preventDefault();
+        this.tab = evt.nextId;
+        this.routeToTab();
+    }
+
+    routeToTab() {
+        const url =
+            `/staff/cat/volcopy/${this.tab}/${this.target}/${this.targetId}`;
+
+        // Retain search parameters
+        this.router.navigate([url], {queryParamsHandling: 'merge'});
+    }
+
     fetchSession(session: string): Promise<any> {
 
         return this.cache.getItem(session, 'edit-these-copies')
index abaa8b8..d78663b 100644 (file)
@@ -10,6 +10,8 @@ import {AuthService} from '@eg/core/auth.service';
 import {VolCopyContext} from './volcopy';
 import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
+import {StoreService} from '@eg/core/store.service';
+import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
 
 /* Managing volcopy data */
 
@@ -21,6 +23,19 @@ export class VolCopyService {
     defaultValues: any = null;
     copyStatuses: {[id: number]: IdlObject} = null;
 
+    // Track this here so it can survive route changes.
+    currentContext: VolCopyContext;
+
+    ageProtectRules: IdlObject[] = [];
+    floatingGroups: IdlObject[] = [];
+    itemTypeMaps: IdlObject[] = [];
+    circModifiers: IdlObject[] = [];
+    statCats: IdlObject[] = [];
+    statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
+
+    templateNames: ComboboxEntry[] = [];
+    templates: any = {};
+
     constructor(
         private evt: EventService,
         private net: NetService,
@@ -29,13 +44,112 @@ export class VolCopyService {
         private auth: AuthService,
         private pcrud: PcrudService,
         private holdings: HoldingsService,
-        private store: ServerStoreService
+        private store: StoreService,
+        private serverStore: ServerStoreService
     ) {}
 
+
+    // Fetch the data that is always needed.
+    load(): Promise<any> {
+
+        if (this.itemTypeMaps.length > 0) {
+            return Promise.resolve();
+        }
+
+        return this.fetchDefaults()
+        .then(_ => this.holdings.fetchCallNumberClasses())
+        .then(_ => this.holdings.fetchCallNumberPrefixes())
+        .then(_ => this.holdings.fetchCallNumberSuffixes())
+        .then(_ => this.fetchCopyStats())
+        .then(_ => this.fetchTemplates())
+        .then(_ => {
+
+            return this.pcrud.retrieveAll('crahp')
+            .pipe(tap(rule => this.ageProtectRules.push(rule))).toPromise()
+
+        }).then(_ => {
+
+            this.ageProtectRules = this.ageProtectRules.sort(
+                (a, b) => a.name() < b.name() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.pcrud.retrieveAll('cfg')
+            .pipe(tap(rule => this.floatingGroups.push(rule))).toPromise();
+
+        }).then(_ => {
+
+            this.floatingGroups = this.floatingGroups.sort(
+                (a, b) => a.name() < b.name() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.pcrud.retrieveAll('ccm')
+            .pipe(tap(rule => this.circModifiers.push(rule))).toPromise();
+
+        }).then(_ => {
+
+            this.circModifiers = this.circModifiers.sort(
+                (a, b) => a.name() < b.name() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.pcrud.retrieveAll('citm')
+            .pipe(tap(itemType => this.itemTypeMaps.push(itemType))).toPromise();
+
+        }).then(_ => {
+
+            this.itemTypeMaps = this.itemTypeMaps.sort(
+                (a, b) => a.value() < b.value() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.net.request('open-ils.circ',
+                'open-ils.circ.stat_cat.asset.retrieve.all',
+                this.auth.token(), this.auth.user().ws_ou()
+            ).toPromise().then(stats => this.statCats = stats);
+
+        }).then(_ => {
+
+            // Sort most local to the front of the list.
+            this.statCats = this.statCats.sort((s1, s2) => {
+                const d1 = this.org.get(s1.owner()).ou_type().depth();
+                const d2 = this.org.get(s2.owner()).ou_type().depth();
+
+                if (d1 > d2) {
+                    return -1;
+                } else if (d1 < d2) {
+                    return 1;
+                } else {
+                    return s1.name() < s2.name() ? -1 : 1;
+                }
+            });
+
+            this.statCats.forEach(cat => {
+                cat.entries().forEach(
+                    entry => this.statCatEntryMap[entry.id()] = entry);
+            });
+        });
+    }
+
+    fetchTemplates(): Promise<any> {
+
+        // TODO: copy templates should be server settings
+        const tmpls = this.store.getLocalItem('cat.copy.templates');
+        if (!tmpls) { return Promise.resolve(); }
+
+        this.templates = tmpls;
+        this.templateNames = Object.keys(tmpls)
+        .sort((n1, n2) => n1 < n2 ? -1 : 1)
+        .map(name => ({id: name, label: name}));
+
+        return Promise.resolve();
+    }
+
     fetchDefaults(): Promise<any> {
         if (this.defaultValues) { return Promise.resolve(); }
 
-        return this.store.getItem('cat.copy.defaults').then(
+        return this.serverStore.getItem('cat.copy.defaults').then(
             defaults => {
                 this.defaultValues = defaults || {};
             }
@@ -254,5 +368,8 @@ export class VolCopyService {
 
         return promise;
     }
+
+
+
 }
 
index d1144fd..7780680 100644 (file)
@@ -225,6 +225,18 @@ body>.dropdown-menu {z-index: 2100;}
   color: black;
 }
 
+/* Washed out version of the Bootstrap 'info' background.
+ * Useful for blocking out sections of a page/form without it 
+ * being so intensely colorful */
+.bg-faint {
+  /*background-color: rgb(204, 229, 255, 0.3);*/
+
+  /* d9edf7 */
+  /*background-color: rgb(217, 237, 247, 0.5);*/
+
+  background-color: rgba(0,0,0,.03);
+}
+
 /* Allow for larger XL dialogs */
 @media (min-width: 1300px) { .modal-xl { max-width: 1200px; } }
 @media (min-width: 1600px) { .modal-xl { max-width: 1500px; } }