1 import {Injectable, EventEmitter} from '@angular/core';
2 import {Observable} from 'rxjs';
3 import {map, tap, distinct} from 'rxjs/operators';
4 import {StoreService} from '@eg/core/store.service';
5 import {IdlObject} from '@eg/core/idl.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {NetService} from '@eg/core/net.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
10 import {MARC_RECORD_TYPE} from './editor-context';
12 const DEFAULT_MARC_FORMAT = 'marc21';
14 interface TagTableSelector {
16 marcRecordType: MARC_RECORD_TYPE;
18 // MARC record fixed field "Type" value.
22 export class TagTable {
29 selector: TagTableSelector;
31 // Current set of tags in list and map form.
32 tagMap: {[tag: string]: any};
35 fieldTags: ContextMenuEntry[];
37 // Cache of compiled, sorted, munged data. Stuff the UI requests
38 // frequently for selectors, etc.
39 cache: {[valueType: string]: {[which: string]: any}} = {
51 selector: TagTableSelector
57 this.selector = selector;
61 load(): Promise<any> {
65 this.getFfValueTable(),
69 // Various data needs munging for display. Cached the modified
70 // values since they are refernced repeatedly by the UI code.
71 fromCache(dataType: string, which?: string, which2?: string): ContextMenuEntry[] {
72 const part1 = this.cache[dataType][which];
82 toCache(dataType: string, which: string,
83 which2: string, values: ContextMenuEntry[]): ContextMenuEntry[] {
84 const base = this.cache[dataType];
85 const part1 = base[which];
88 if (!base[which]) { base[which] = {}; }
89 base[which][which2] = values;
97 getFfPosTable(): Promise<any> {
98 const storeKey = 'FFPosTable_' + this.selector.ffType;
100 if (this.ffPosTable) {
101 return Promise.resolve(this.ffPosTable);
104 this.ffPosTable = this.store.getLocalItem(storeKey);
106 if (this.ffPosTable) {
107 return Promise.resolve(this.ffPosTable);
110 return this.net.request(
111 'open-ils.fielder', 'open-ils.fielder.cmfpm.atomic',
112 {query: {tag: {'!=' : '006'}, rec_type: this.selector.ffType}}
114 ).toPromise().then(table => {
115 this.store.setLocalItem(storeKey, table);
116 return this.ffPosTable = table;
120 // ffType is the fixed field Type value. BKS, AUT, etc.
121 // See config.marc21_rec_type_map
122 getFfValueTable(): Promise<any> {
124 const storeKey = 'FFValueTable_' + this.selector.ffType;
126 if (this.ffValueTable) {
127 return Promise.resolve(this.ffValueTable);
130 this.ffValueTable = this.store.getLocalItem(storeKey);
132 if (this.ffValueTable) {
133 return Promise.resolve(this.ffValueTable);
136 return this.net.request(
138 'open-ils.cat.biblio.fixed_field_values.by_rec_type',
141 ).toPromise().then(table => {
142 this.store.setLocalItem(storeKey, table);
143 return this.ffValueTable = table;
147 loadTagTable(): Promise<any> {
149 const sel = this.selector;
152 `current_tag_table_${sel.marcFormat}_${sel.marcRecordType}`;
154 this.tagMap = this.store.getLocalItem(cacheKey);
157 return Promise.resolve(this.tagMap);
160 return this.fetchTagTable().then(_ => {
161 this.store.setLocalItem(cacheKey, this.tagMap);
166 fetchTagTable(): Promise<any> {
168 return this.net.request(
170 'open-ils.cat.tag_table.all.retrieve.local',
171 this.auth.token(), this.selector.marcFormat,
172 this.selector.marcRecordType
173 ).pipe(tap(tagData => {
174 this.tagMap[tagData.tag] = tagData;
178 getSubfieldCodes(tag: string): ContextMenuEntry[] {
179 if (!tag || !this.tagMap[tag]) { return null; }
181 const cached = this.fromCache('sfcodes', tag);
183 const list = this.tagMap[tag].subfields.map(sf => ({
185 label: `${sf.code}: ${sf.description}`
187 .sort((a, b) => a.label < b.label ? -1 : 1);
189 return this.toCache('sfcodes', tag, null, list);
192 getSubfieldLabel(tag: string, sfCode: string): string {
193 if (!tag || !this.tagMap[tag]) { return null; }
194 const subfieldResults = this.tagMap[tag].subfields.filter(sf => sf.code === sfCode);
195 return subfieldResults.length ? subfieldResults[0].description : null;
199 getFieldTags(): ContextMenuEntry[] {
201 if (!this.fieldTags) {
202 this.fieldTags = Object.keys(this.tagMap)
203 .filter(tag => Boolean(this.tagMap[tag]))
206 label: `${tag}: ${this.tagMap[tag].name}`
208 .sort((a, b) => a.label < b.label ? -1 : 1);
211 return this.fieldTags;
214 getSubfieldValues(tag: string, sfCode: string): ContextMenuEntry[] {
215 if (!tag || !this.tagMap[tag]) { return []; }
217 const cached = this.fromCache('sfvalues', tag, sfCode);
218 if (cached) { return cached; }
220 const list: ContextMenuEntry[] = [];
222 this.tagMap[tag].subfields
224 sf.code === sfCode && sf.hasOwnProperty('value_list'))
226 sf.value_list.forEach(value => {
228 let label = value.description || value.code;
229 const code = value.code || label;
230 if (code !== label) { label = `${code}: ${label}`; }
232 list.push({value: code, label: label});
236 return this.toCache('sfvalues', tag, sfCode, list);
239 getIndicatorValues(tag: string, which: 'ind1' | 'ind2'): ContextMenuEntry[] {
240 if (!tag || !this.tagMap[tag]) { return; }
242 const cached = this.fromCache('indicators', tag, which);
243 if (cached) { return cached; }
245 let values = this.tagMap[tag][which];
246 if (!values) { return; }
248 values = values.map(value => ({
250 label: `${value.code}: ${value.description}`
252 .sort((a, b) => a.label < b.label ? -1 : 1);
254 return this.toCache('indicators', tag, which, values);
258 getFfFieldMeta(fieldCode: string): Promise<IdlObject> {
259 return this.getFfPosTable().then(table => {
261 // Best I can tell, the AngJS MARC editor stores the
262 // full POS table for all record types in every copy of
263 // the table, hence the seemingly extraneous check in ffType.
266 field.fixed_field === fieldCode
267 && field.rec_type === this.selector.ffType
273 // Assumes getFfPosTable and getFfValueTable have already been
274 // invoked for the requested record type.
275 getFfValues(fieldCode: string): ContextMenuEntry[] {
277 const cached = this.fromCache('ffvalues', fieldCode);
278 if (cached) { return cached; }
280 let values = this.ffValueTable;
282 if (!values || !values[fieldCode]) { return null; }
284 // extract the canned set of possible values for our
285 // fixed field. Ignore those whose value exceeds the
286 // specified field length.
287 values = values[fieldCode]
288 .filter(val => val[0].length <= val[2])
289 .map(val => ({value: val[0], label: `${val[0]}: ${val[1]}`}))
290 .sort((a, b) => a.label < b.label ? -1 : 1);
292 return this.toCache('ffvalues', fieldCode, null, values);
298 export class TagTableService {
300 tagTables: {[marcRecordType: string]: TagTable} = {};
301 controlledBibTags: string[];
304 private store: StoreService,
305 private auth: AuthService,
306 private net: NetService,
307 private pcrud: PcrudService,
310 loadTags(selector: TagTableSelector): Promise<TagTable> {
311 if (!selector.marcFormat) {
312 selector.marcFormat = DEFAULT_MARC_FORMAT;
315 // Tag tables of a given marc record type are identical.
316 if (this.tagTables[selector.marcRecordType]) {
317 return Promise.resolve(this.tagTables[selector.marcRecordType]);
320 const tt = new TagTable(
321 this.store, this.auth, this.net, this.pcrud, selector);
323 this.tagTables[selector.marcRecordType] = tt;
325 return tt.load().then(_ => tt);
328 getControlledBibTags(): Promise<string[]> {
329 if (this.controlledBibTags) {
330 return Promise.resolve(this.controlledBibTags);
333 this.controlledBibTags = [];
334 return this.pcrud.retrieveAll('acsbf', {select: ['tag']})
336 map(field => field.tag()),
338 map(tag => this.controlledBibTags.push(tag))
339 ).toPromise().then(_ => this.controlledBibTags);