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