LP1825851 Print template admin misc. repairs/improvements
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / admin / server / print-template.component.ts
1 import {Component, OnInit, ViewChild, TemplateRef} from '@angular/core';
2 import {Observable} from 'rxjs';
3 import {map} from 'rxjs/operators';
4 import {ActivatedRoute} from '@angular/router';
5 import {IdlService, IdlObject} from '@eg/core/idl.service';
6 import {PcrudService} from '@eg/core/pcrud.service';
7 import {AuthService} from '@eg/core/auth.service';
8 import {OrgService} from '@eg/core/org.service';
9 import {ComboboxComponent, ComboboxEntry
10     } from '@eg/share/combobox/combobox.component';
11 import {PrintService} from '@eg/share/print/print.service';
12 import {LocaleService} from '@eg/core/locale.service';
13 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
14 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
15 import {SampleDataService} from '@eg/share/util/sample-data.service';
16 import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
17 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
18
19 /**
20  * Print Template Admin Page
21  */
22
23 @Component({
24     templateUrl: 'print-template.component.html'
25 })
26
27 export class PrintTemplateComponent implements OnInit {
28
29     entries: ComboboxEntry[];
30     template: IdlObject;
31     sampleJson: string;
32     invalidJson = false;
33     localeCode: string;
34     localeEntries: ComboboxEntry[];
35     compiledContent: string;
36     templateCache: {[id: number]: IdlObject} = {};
37     initialOrg: number;
38     selectedOrgs: number[];
39
40     @ViewChild('templateSelector') templateSelector: ComboboxComponent;
41     @ViewChild('tabs') tabs: NgbTabset;
42     @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
43     @ViewChild('confirmDelete') confirmDelete: ConfirmDialogComponent;
44
45     // Define some sample data that can be used for various templates
46     // Data will be filled out via the sample data service.
47     // Keys map to print template names
48     sampleData: any = {
49         patron_address: {},
50         holds_for_bib: {}
51     };
52
53     constructor(
54         private route: ActivatedRoute,
55         private idl: IdlService,
56         private org: OrgService,
57         private pcrud: PcrudService,
58         private auth: AuthService,
59         private locale: LocaleService,
60         private printer: PrintService,
61         private samples: SampleDataService
62     ) {
63         this.entries = [];
64         this.localeEntries = [];
65     }
66
67     ngOnInit() {
68         this.initialOrg = this.auth.user().ws_ou();
69         this.selectedOrgs = [this.initialOrg];
70         this.localeCode = this.locale.currentLocaleCode();
71         this.locale.supportedLocales().subscribe(
72             l => this.localeEntries.push({id: l.code(), label: l.name()}));
73         this.setTemplateInfo().subscribe();
74         this.fleshSampleData();
75     }
76
77     fleshSampleData() {
78
79         // NOTE: server templates work fine with IDL objects, but
80         // vanilla hashes are easier to work with in the admin UI.
81
82         // Classes for which sample data exists
83         const classes = ['au', 'ac', 'aua', 'ahr', 'acp', 'mwde'];
84         const samples: any = {};
85         classes.forEach(class_ => samples[class_] =
86             this.idl.toHash(this.samples.listOfThings(class_, 10)));
87
88         // Wide holds are hashes instead of IDL objects.
89         // Add fields as needed.
90         const wide_holds = [{
91             request_time: this.samples.randomDate().toISOString(),
92             ucard_barcode: samples.ac[0].barcode,
93             usr_family_name: samples.au[0].family_name,
94             usr_alias: samples.au[0].alias,
95             cp_barcode: samples.acp[0].barcode
96         }, {
97             request_time: this.samples.randomDate().toISOString(),
98             ucard_barcode: samples.ac[1].barcode,
99             usr_family_name: samples.au[1].family_name,
100             usr_alias: samples.au[1].alias,
101             cp_barcode: samples.acp[1].barcode
102         }];
103
104         this.sampleData.patron_address = {
105             patron:  samples.au[0],
106             address: samples.aua[0]
107         };
108
109         this.sampleData.holds_for_bib = wide_holds;
110     }
111
112     onTabChange(evt: NgbTabChangeEvent) {
113         if (evt.nextId === 'template') {
114             this.refreshPreview();
115         }
116     }
117
118     container(): any {
119         // Only present when its tab is visible
120         return document.getElementById('template-preview-pane');
121     }
122
123     // TODO should the ngModelChange handler fire for org-family-select
124     // even when the values don't change?
125     orgOnChange(family: OrgFamily) {
126         // Avoid reundant server calls.
127         if (!this.sameIds(this.selectedOrgs, family.orgIds)) {
128             this.selectedOrgs = family.orgIds;
129             this.setTemplateInfo().subscribe();
130         }
131     }
132
133     // True if the 2 arrays contain the same contents,
134     // regardless of the order.
135     sameIds(arr1: any[], arr2: any[]): boolean {
136         if (arr1.length !== arr2.length) {
137             return false;
138         }
139         for (let i = 0; i < arr1.length; i++) {
140             if (!arr2.includes(arr1[i])) {
141                 return false;
142             }
143         }
144         return true;
145     }
146
147     localeOnChange(code: string) {
148         if (code) {
149             this.localeCode = code;
150             this.setTemplateInfo().subscribe();
151         }
152     }
153
154     // Fetch name/id for all templates in range.
155     // Avoid fetching the template content until needed.
156     setTemplateInfo(): Observable<IdlObject> {
157         this.entries = [];
158         this.template = null;
159         this.templateSelector.applyEntryId(null);
160         this.compiledContent = '';
161
162         return this.pcrud.search('cpt',
163             {
164                 owner: this.selectedOrgs,
165                 locale: this.localeCode
166             }, {
167                 select: {cpt: ['id', 'label', 'owner']},
168                 order_by: {cpt: 'label'}
169             }
170         ).pipe(map(tmpl => {
171             this.templateCache[tmpl.id()] = tmpl;
172             this.entries.push({id: tmpl.id(), label: tmpl.label()});
173             return tmpl;
174         }));
175     }
176
177     getOwnerName(id: number): string {
178         if (this.templateCache[id]) {
179             return this.org.get(this.templateCache[id].owner()).shortname();
180         }
181         return '';
182     }
183
184     // If the selected template changes through means other than the
185     // template selecdtor, setting updateSelector=true will force the
186     // template to appear in the selector and get selected, regardless
187     // of whether it would have been fetched with current filters.
188     selectTemplate(id: number, updateSelector?: boolean) {
189
190         if (id === null) {
191             this.template = null;
192             this.compiledContent = '';
193             return;
194         }
195
196         this.pcrud.retrieve('cpt', id).subscribe(t => {
197             this.template = this.templateCache[id] = t;
198
199             if (updateSelector) {
200                 if (!this.templateSelector.hasEntry(id)) {
201                     this.templateSelector.addEntry({id: id, label: t.label()});
202                 }
203                 this.templateSelector.applyEntryId(id);
204             }
205
206             const data = this.sampleData[t.name()];
207             if (data) {
208                 this.sampleJson = JSON.stringify(data, null, 2);
209                 this.refreshPreview();
210             }
211         });
212     }
213
214     // Allow the template editor textarea to expand vertically as
215     // content is added, with a sane minimum row count
216     templateRowCount(): number {
217         const def = 25;
218         if (this.template && this.template.template()) {
219             return Math.max(def,
220                 this.template.template().split(/\n/).length + 2);
221         }
222         return def;
223     }
224
225     refreshPreview() {
226         if (!this.sampleJson) { return; }
227         this.compiledContent = '';
228
229         let data;
230         try {
231             data = JSON.parse(this.sampleJson);
232             this.invalidJson = false;
233         } catch (E) {
234             this.invalidJson = true;
235         }
236
237         this.printer.compileRemoteTemplate({
238             templateId: this.template.id(),
239             contextData: data,
240             printContext: 'default' // required, has no impact here
241
242         }).then(response => {
243
244             this.compiledContent = response.content;
245             if (this.container()) { // null if on alternate tab
246                 if (response.contentType === 'text/html') {
247                     this.container().innerHTML = response.content;
248                 } else {
249                     // Assumes text/plain or similar
250                     this.container().innerHTML = '<pre>' + response.content + '</pre>';
251                 }
252             }
253         });
254     }
255
256     applyChanges() {
257         this.container().innerHTML = '';
258         this.pcrud.update(this.template).toPromise()
259             .then(() => this.refreshPreview());
260     }
261
262     openEditDialog() {
263         this.editDialog.setRecord(this.template);
264         this.editDialog.mode = 'update';
265         this.editDialog.open({size: 'lg'}).toPromise().then(id => {
266             if (id !== undefined) {
267                 const selectedId = this.template.id();
268                 this.setTemplateInfo().toPromise().then(
269                     _ => this.selectTemplate(selectedId)
270                 );
271             }
272         });
273     }
274
275     cloneTemplate() {
276         const tmpl = this.idl.clone(this.template);
277         tmpl.id(null);
278         tmpl.active(false); // Cloning requires manual activation
279         tmpl.owner(null);
280         this.editDialog.setRecord(tmpl);
281         this.editDialog.mode = 'create';
282         this.editDialog.open({size: 'lg'}).toPromise().then(newTmpl => {
283             if (newTmpl !== undefined) {
284                 this.setTemplateInfo().toPromise()
285                     .then(_ => this.selectTemplate(newTmpl.id(), true));
286             }
287         });
288     }
289
290     deleteTemplate() {
291         this.confirmDelete.open().subscribe(confirmed => {
292             if (!confirmed) { return; }
293             this.pcrud.remove(this.template).toPromise().then(_ => {
294                 this.setTemplateInfo().toPromise()
295                     .then(x => this.selectTemplate(null));
296             });
297         });
298     }
299 }
300
301