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 {NgbNav, NgbNavChangeEvent} 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';
21 * Print Template Admin Page
25 templateUrl: 'print-template.component.html'
28 export class PrintTemplateComponent implements OnInit {
30 entries: ComboboxEntry[];
35 localeEntries: ComboboxEntry[];
36 compiledContent: string;
37 templateCache: {[id: number]: IdlObject} = {};
39 selectedOrgs: number[];
40 selectedTab = 'template';
42 @ViewChild('templateSelector', { static: true }) templateSelector: ComboboxComponent;
43 @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
44 @ViewChild('confirmDelete', { static: true }) confirmDelete: ConfirmDialogComponent;
45 @ViewChild('printContextCbox', {static: false}) printContextCbox: ComboboxComponent;
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
59 private route: ActivatedRoute,
60 private idl: IdlService,
61 private org: OrgService,
62 private pcrud: PcrudService,
63 private auth: AuthService,
64 private store: ServerStoreService,
65 private locale: LocaleService,
66 private printer: PrintService,
67 private samples: SampleDataService
70 this.localeEntries = [];
74 this.initialOrg = this.auth.user().ws_ou();
75 this.selectedOrgs = [this.initialOrg];
76 this.localeCode = this.locale.currentLocaleCode();
77 this.locale.supportedLocales().subscribe(
78 l => this.localeEntries.push({id: l.code(), label: l.name()}));
79 this.setTemplateInfo().subscribe();
80 this.fleshSampleData();
85 // NOTE: server templates work fine with IDL objects, but
86 // vanilla hashes are easier to work with in the admin UI.
88 // Classes for which sample data exists
89 const classes = ['au', 'ac', 'aua', 'ahr', 'acp', 'mwde', 'mbt', 'mbts'];
90 const samples: any = {};
91 classes.forEach(class_ => samples[class_] =
92 this.idl.toHash(this.samples.listOfThings(class_, 10)));
94 // Wide holds are hashes instead of IDL objects.
95 // Add fields as needed.
97 request_time: this.samples.randomDateIso(),
98 ucard_barcode: samples.ac[0].barcode,
99 usr_family_name: samples.au[0].family_name,
100 usr_alias: samples.au[0].alias,
101 cp_barcode: samples.acp[0].barcode
103 request_time: this.samples.randomDateIso(),
104 ucard_barcode: samples.ac[1].barcode,
105 usr_family_name: samples.au[1].family_name,
106 usr_alias: samples.au[1].alias,
107 cp_barcode: samples.acp[1].barcode
110 this.sampleData.patron_address = {
111 patron: samples.au[0],
112 address: samples.aua[0]
115 const patron = this.idl.clone(samples.au[0]);
116 patron.addresses = [samples.aua[0]];
117 patron.stat_cat_entries = [{
118 stat_cat: {name: 'A Stat Cat'},
119 stat_cat_entry: 'A Value'
122 this.sampleData.patron_data = {patron: patron};
124 this.sampleData.holds_for_bib = wide_holds;
127 samples.mbt[0].summary = samples.mbts[0];
128 samples.mbt[1].summary = samples.mbts[1];
129 samples.mbt[2].summary = samples.mbts[2];
131 this.sampleData.bills_current.xacts = [
138 this.sampleData.bills_payment = {
139 previous_balance: 10,
140 payment_type: 'cash_payment',
145 payment_note: 'Test Note',
148 xact: samples.mbt[0],
150 copy_barcode: '3423482302393'
153 xact: samples.mbt[1],
154 title: 'Another Title',
155 copy_barcode: '3423482302394'
159 this.sampleData.hold_shelf_slip = {
161 copy: samples.acp[0],
162 patron: samples.au[0],
167 this.sampleData.hold_transit_slip =
168 Object.assign({}, this.sampleData.hold_shelf_slip);
169 this.sampleData.hold_transit_slip.checkin.destOrg =
173 onNavChange(evt: NgbNavChangeEvent) {
174 if (evt.nextId === 'template') {
175 this.refreshPreview();
180 // Only present when its tab is visible
181 return document.getElementById('template-preview-pane');
184 // TODO should the ngModelChange handler fire for org-family-select
185 // even when the values don't change?
186 orgOnChange(family: OrgFamily) {
187 // Avoid reundant server calls.
188 if (!this.sameIds(this.selectedOrgs, family.orgIds)) {
189 this.selectedOrgs = family.orgIds;
190 this.setTemplateInfo().subscribe();
194 // True if the 2 arrays contain the same contents,
195 // regardless of the order.
196 sameIds(arr1: any[], arr2: any[]): boolean {
197 if (arr1.length !== arr2.length) {
200 for (let i = 0; i < arr1.length; i++) {
201 if (!arr2.includes(arr1[i])) {
208 localeOnChange(code: string) {
210 this.localeCode = code;
211 this.setTemplateInfo().subscribe();
215 // Fetch name/id for all templates in range.
216 // Avoid fetching the template content until needed.
217 setTemplateInfo(): Observable<IdlObject> {
219 this.template = null;
220 this.templateSelector.applyEntryId(null);
221 this.compiledContent = '';
223 return this.pcrud.search('cpt',
225 owner: this.selectedOrgs,
226 locale: this.localeCode
228 select: {cpt: ['id', 'label', 'owner']},
229 order_by: {cpt: 'label'}
232 this.templateCache[tmpl.id()] = tmpl;
233 this.entries.push({id: tmpl.id(), label: tmpl.label()});
238 getOwnerName(id: number): string {
239 if (this.templateCache[id]) {
240 return this.org.get(this.templateCache[id].owner()).shortname();
245 // If the selected template changes through means other than the
246 // template selector, setting updateSelector=true will force the
247 // template to appear in the selector and get selected, regardless
248 // of whether it would have been fetched with current filters.
249 selectTemplate(id: number, updateSelector?: boolean) {
252 this.template = null;
253 this.compiledContent = '';
258 this.selectedTab = 'template';
259 this.compiledContent = '';
260 if (this.container()) {
261 this.container().innerHTML = '';
263 this.sampleJson = '';
265 this.pcrud.retrieve('cpt', id).subscribe(t => {
266 this.template = this.templateCache[id] = t;
268 if (updateSelector) {
269 if (!this.templateSelector.hasEntry(id)) {
270 this.templateSelector.addEntry({id: id, label: t.label()});
272 this.templateSelector.applyEntryId(id);
275 const data = this.sampleData[t.name()];
277 this.sampleJson = JSON.stringify(data, null, 2);
278 this.refreshPreview();
281 this.store.getItem('eg.print.template_context.' + this.template.name())
283 this.printContextCbox.applyEntryId(setting || 'unset');
288 // Allow the template editor textarea to expand vertically as
289 // content is added, with a sane minimum row count
290 templateRowCount(): number {
292 if (this.template && this.template.template()) {
294 this.template.template().split(/\n/).length + 2);
300 if (!this.sampleJson) { return; }
301 this.compiledContent = '';
305 data = JSON.parse(this.sampleJson);
306 this.invalidJson = false;
308 this.invalidJson = true;
311 this.printer.compileRemoteTemplate({
312 templateId: this.template.id(),
314 printContext: 'default' // required, has no impact here
316 }).then(response => {
318 this.compiledContent = response.content;
319 if (this.container()) { // null if on alternate tab
320 if (response.contentType === 'text/html') {
321 this.container().innerHTML = response.content;
323 // Assumes text/plain or similar
324 this.container().innerHTML = '<pre>' + response.content + '</pre>';
331 this.container().innerHTML = '';
332 this.pcrud.update(this.template).toPromise()
333 .then(() => this.refreshPreview());
337 this.editDialog.setRecord(this.template);
338 this.editDialog.mode = 'update';
339 this.editDialog.open({size: 'lg'}).toPromise().then(id => {
340 if (id !== undefined) {
341 const selectedId = this.template.id();
342 this.setTemplateInfo().toPromise().then(
343 _ => this.selectTemplate(selectedId)
350 const tmpl = this.idl.clone(this.template);
352 tmpl.active(false); // Cloning requires manual activation
354 this.editDialog.setRecord(tmpl);
355 this.editDialog.mode = 'create';
356 this.editDialog.open({size: 'lg'}).toPromise().then(newTmpl => {
357 if (newTmpl !== undefined) {
358 this.setTemplateInfo().toPromise()
359 .then(_ => this.selectTemplate(newTmpl.id(), true));
365 this.confirmDelete.open().subscribe(confirmed => {
366 if (!confirmed) { return; }
367 this.pcrud.remove(this.template).toPromise().then(_ => {
368 this.setTemplateInfo().toPromise()
369 .then(x => this.selectTemplate(null));
374 forceContextChange(entry: ComboboxEntry) {
375 if (entry && entry.id !== 'unset') {
378 'eg.print.template_context.' + this.template.name(), entry.id);
382 this.store.removeItem(
383 'eg.print.template_context.' + this.template.name());