<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>
+ <eg-combobox #copyTemplateCbox domId="template-select"
+ [allowFreeText]="true" [entries]="volcopy.templateNames"
+ [(ngModel)]="currentTemplate">
+ </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>
+ -->
+
+ <!--
+ The type typical approach of wrapping a file input in a <label>
+ results in button-ish things that have slightly different dimensions
+ -->
+ <button class="btn btn-outline-dark mr-2" (click)="templateFile.click()">
+ <input type="file" class="d-none" #templateFile
+ (change)="importTemplate($event)" id="template-file-upload"/>
+ <span i18n>Import</span>
+ </button>
+
+ <a (click)="exportTemplate($event)"
+ download="export_copy_template.json" [href]="exportTemplateUrl()">
+ <button class="btn btn-outline-dark mr-2" i18n>Export</button>
+ </a>
+
<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>
+ <button class="btn btn-outline-danger mr-2"
+ (click)="deleteTemplate()" i18n>Delete Template</button>
</div>
</div>
</div>
<div *ngIf="displayAttr('owning_lib')">
+ <eg-string #olLabel text="Owning Library" i18n-text></eg-string>
<ng-template #owningLibTemplate>
<eg-org-select
domId="owning-lib-input"
</eg-org-select>
</ng-template>
<ng-container *ngTemplateOutlet="batchAttr;
- context:{field:'owning_lib',template:owningLibTemplate}">
+ context:{field:'owning_lib',template:owningLibTemplate,label:olLabel.text}">
</ng-container>
</div>
[emptyIsUnset]="true"
[editTemplate]="statCatTemplate"
[labelCounts]="statCatCounts(cat.id())"
+ (valueCleared)="statCatChanged(cat.id(), true)"
(changesSaved)="statCatChanged(cat.id())">
</eg-batch-item-attr>
</div>
import {Component, Input, OnInit, AfterViewInit, ViewChild,
QueryList, ViewChildren} from '@angular/core';
import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {SafeUrl} from '@angular/platform-browser';
import {tap} from 'rxjs/operators';
import {IdlObject, IdlService} from '@eg/core/idl.service';
import {EventService} from '@eg/core/event.service';
} from '@eg/staff/share/holdings/copy-alerts-dialog.component';
import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {BatchItemAttrComponent} from '@eg/staff/share/holdings/batch-item-attr.component';
+import {FileExportService} from '@eg/share/util/file-export.service';
@Component({
selector: 'eg-copy-attrs',
private holdings: HoldingsService,
private format: FormatService,
private store: StoreService,
+ private fileExport: FileExportService,
public volcopy: VolCopyService
) { }
ngAfterViewInit() {
const tmpl = this.store.getLocalItem('cat.copy.last_template');
- if (tmpl) { this.copyTemplateCbox.selectedId = tmpl; }
+ if (tmpl) {
+ // avoid Express Changed warning w/ timeout
+ setTimeout(() => this.copyTemplateCbox.selectedId = tmpl);
+ }
this.loanDurationLabelMap[1] = this.loanDurationShort.text;
this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
this.values[field] = value;
}
- // TODO: handle circ_lib, owning_lib changes specially
-
- console.debug('APPLYING', field, value);
+ if (field === 'owning_lib') {
+ return this.owningLibChanged(value);
+ }
this.context.copyList().forEach(copy => {
if (copy[field] && copy[field]() !== value) {
});
}
+ owningLibChanged(orgId: number) {
+ if (!orgId) { return; }
+
+ let promise = Promise.resolve();
+
+ // Map existing vol IDs to their replacments.
+ const newVols: any = {};
+
+ this.context.copyList().forEach(copy => {
+
+ // Change the copy circ lib to match the new owning lib
+ // if configured to do so.
+ if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) {
+ if (copy.circ_lib() !== orgId) {
+ copy.circ_lib(orgId);
+ copy.ischanged(true);
+
+ this.batchAttrs
+ .filter(ba => ba.name === 'circ_lib')
+ .forEach(attr => attr.hasChanged = true);
+ }
+ }
+
+ const vol = copy.call_number();
+
+ if (vol.owning_lib() === orgId) { return; } // No change needed
+
+ let newVol;
+ if (newVols[vol.id()]) {
+ newVol = newVols[vol.id()];
+
+ } else {
+
+ // The open-ils.cat.asset.volume.fleshed.batch.update API
+ // will use the existing volume when trying to create a
+ // new volume with the same parameters as an existing volume.
+ newVol = this.idl.clone(vol);
+ newVol.owning_lib(orgId);
+ newVol.id(this.volcopy.autoId--);
+ newVol.isnew(true);
+ newVols[vol.id()] = newVol;
+ }
+
+ copy.call_number(newVol);
+ copy.ischanged();
+
+ this.context.removeCopyNode(copy.id());
+ this.context.findOrCreateCopyNode(copy);
+ });
+
+ // If any of the above actions results in an empty volume
+ // remove it from the tree. Note this does not delete the
+ // volume at the server, since other items could be attached
+ // of which this instance of the editor is not aware.
+ Object.keys(newVols).forEach(volId => {
+ const volNode = this.context.volNodes().filter(
+ volNode => volNode.target.id() === +volId)[0];
+
+ if (volNode && volNode.children.length === 0) {
+ this.context.removeVolNode(+volId);
+ }
+ });
+ }
+
// Create or modify a stat cat entry for each copy that does not
// already match the new value.
- statCatChanged(catId: number) {
+ statCatChanged(catId: number, clear?: boolean) {
catId = Number(catId);
const entryId = this.statCatValues[catId];
+
this.context.copyList().forEach(copy => {
let entry = copy.stat_cat_entries()
.filter(e => e.stat_cat() === catId)[0];
+ console.log('0', entry);
if (entry) {
if (entry.id() === entryId) {
entry = this.idl.create('asce');
entry.stat_cat(catId);
copy.stat_cat_entries().push(entry);
+ console.log('1', entry);
}
entry.id(entryId);
this.store.setLocalItem('cat.copy.last_template', entry.id);
+ // TODO: handle owning_lib
+
const template = this.volcopy.templates[entry.id];
Object.keys(template).forEach(field => {
if (value === null || value === undefined) { return; }
- this.applyCopyValue(field, value);
+ if (field === 'statcats') {
+ Object.keys(value).forEach(catId => {
+ this.statCatValues[+catId] = value[+catId];
+ this.statCatChanged(+catId);
+ });
+ return;
+ }
+
+ // In some cases, we may have to fetch the data since
+ // the local code assumes copy field is fleshed.
+ let promise = Promise.resolve(value);
+
+ if (field === 'location') {
+ // May be a 'remote' location. Fetch as needed.
+ promise = this.volcopy.getLocation(value);
+ }
+
+ promise.then(val => {
+ this.applyCopyValue(field, val);
+
+ // Indicate in the form these values have changed
+ this.batchAttrs
+ .filter(ba => ba.name === field)
+ .forEach(attr => attr.hasChanged = true);
+ });
});
}
saveTemplate() {
+ const entry: ComboboxEntry = this.copyTemplateCbox.selected;
+ if (!entry) { return; }
+
+ let name;
+ let template;
+
+ if (entry.freetext) {
+ name = entry.label;
+ // freetext entries don't have an ID, but we may need one later.
+ entry.id = entry.label;
+ template = {};
+ } else {
+ name = entry.id;
+ template = this.volcopy.templates[name];
+ }
+
+ // Changes will have applied to all items.
+ const copy = this.context.copyList()[0];
+
this.batchAttrs.forEach(comp => {
- console.log(comp.editInputDomId);
+ if (!comp.hasChanged) { return; }
+
+ const value = copy[comp.name]();
+ const name = comp.name;
+
+ if (value === null) {
+ delete template[name];
+ return;
+ }
+
+ if (name.match(/stat_cat_/)) {
+ const statId = name.match(/stat_cat_(\d+)/)[1];
+ if (!template.statcats) { template.statcats = {}; }
+
+ template.statcats[statId] = value;
+
+ } else {
+
+ // some values are fleshed.
+ // this assumes fleshed objects have an 'id' value,
+ // which is true so far.
+ template[name] =
+ typeof value === 'object' ? value.id() : value;
+ }
});
+
+ this.volcopy.templates[name] = template;
+ this.volcopy.saveTemplates();
+ }
+
+ exportTemplate($event) {
+ if (this.fileExport.inProgress()) { return; }
+
+ this.fileExport.exportFile(
+ $event, JSON.stringify(this.volcopy.templates), 'text/json');
+ }
+
+ importTemplate($event) {
+ const file: File = $event.target.files[0];
+ if (!file) { return; }
+
+ const reader = new FileReader();
+
+ reader.addEventListener('load', () => {
+
+ try {
+ const template = JSON.parse(reader.result as string);
+ const name = Object.keys(template)[0];
+ this.volcopy.templates[name] = template[name];
+ } catch (E) {
+ console.error('Invalid Item Attribute template', E);
+ return;
+ }
+
+ this.volcopy.saveTemplates();
+ // Adds the new one to the list and re-sorts the labels.
+ this.volcopy.fetchTemplates();
+ });
+
+ reader.readAsText(file);
+ }
+
+ // Returns null when no export is in progress.
+ exportTemplateUrl(): SafeUrl {
+ return this.fileExport.safeUrl;
+ }
+
+ deleteTemplate() {
+ const entry: ComboboxEntry = this.copyTemplateCbox.selected;
+ if (!entry) { return; }
+ delete this.volcopy.templates[entry.id];
+ this.volcopy.saveTemplates();
+ this.copyTemplateCbox.selected = null;
}
displayAttr(field: string): boolean {
/* Managing volcopy data */
+
+
interface VolCopyDefaults {
values: {[field: string]: any},
hidden: {[field: string]: boolean}
autoId = -1;
+ localOrgs: number[];
defaults: VolCopyDefaults = null;
defaultLocation: IdlObject;
copyStatuses: {[id: number]: IdlObject} = null;
+ // This will be all 'local' copy locations plus any remote
+ // locations that we are required to interact with.
+ copyLocationMap: {[id: number]: IdlObject} = {};
+
// Track this here so it can survive route changes.
currentContext: VolCopyContext;
if (this.itemTypeMaps.length > 0) { return Promise.resolve(); }
- const myOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
+ this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
return this.fetchDefaults()
+ .then(_ => this.getLocations())
.then(_ => this.holdings.fetchCallNumberClasses())
.then(cls => this.volClasses = cls)
return this.holdings.fetchCallNumberPrefixes().then(prefixes =>
this.volPrefixes = prefixes
.filter(sfx => sfx.id() !== -1)
- .filter(pfx => myOrgs.includes(pfx.owning_lib()))
+ .filter(pfx => this.localOrgs.includes(pfx.owning_lib()))
);
})
return this.holdings.fetchCallNumberSuffixes().then(suffixes =>
this.volSuffixes = suffixes
.filter(sfx => sfx.id() !== -1)
- .filter(sfx => myOrgs.includes(sfx.owning_lib()))
+ .filter(sfx => this.localOrgs.includes(sfx.owning_lib()))
);
})
});
}
+ getLocation(id: number): Promise<IdlObject> {
+ if (this.copyLocationMap[id]) {
+ return Promise.resolve(this.copyLocationMap[id]);
+ }
+
+ return this.pcrud.retrieve('acpl', id)
+ .pipe(tap(loc => this.copyLocationMap[loc.id()] = loc))
+ .toPromise();
+ }
+
+ getLocations(): Promise<any> {
+ this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
+
+ return this.pcrud.search('acpl',
+ {deleted: 'f', owning_lib: this.localOrgs},
+ {order_by: {acpl: 'name'}}
+ ).pipe(tap(loc => this.copyLocationMap[loc.id()] = loc)
+ ).toPromise();
+ }
+
fetchTemplates(): Promise<any> {
// TODO: copy templates should be server settings
return Promise.resolve();
}
+
+ saveTemplates(): Promise<any> {
+ // TODO: templates should be stored on the server.
+ this.store.setLocalItem('cat.copy.templates', this.templates);
+ // Re-sort, etc.
+ return this.fetchTemplates();
+ }
+
fetchDefaults(): Promise<any> {
if (this.defaults) { return Promise.resolve(); }
// Use the first non-deleted copy location within org unit
// range as the default. Typically "Stacks".
- const myOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
+ this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
return this.pcrud.search('acpl',
- {deleted: 'f', owning_lib: myOrgs},
+ {deleted: 'f', owning_lib: this.localOrgs},
{order_by: {acpl: 'id'}, limit: 1}
).toPromise().then(loc => this.defaultLocation = loc);
});