+<ng-template #nodeStrTmpl let-point="point" let-showmatch="showmatch" i18n>
+ <ng-container *ngIf="point">
+ <span *ngIf="point.negate()">NOT </span>
+ <span *ngIf="point.heading()">Normalized Heading</span>
+ <span>{{point.bool_op()}}{{point.svf()}}{{point.tag()}}</span>
+ <span *ngIf="point.subfield()"> ‡{{point.subfield()}}</span>
+ <span *ngIf="showmatch && !point.bool_op()"> | Match score {{point.quality()}}</span>
+ </ng-container>
+</ng-template>
+<eg-string key="staff.cat.vandelay.matchpoint.label"
+ [template]="nodeStrTmpl"></eg-string>
<div class="row mt-2">
- <div class="col-lg-4">
+ <div class="col-lg-7">
+ <div class="row ml-2">
+ <span class="text-white bg-dark p-2" i18n>
+ Your Expression: {{expressionAsString()}}
+ </span>
+ </div>
+ <div class="row ml-2 mt-4">
+ <span class="mr-2" i18n>Add New:</span>
+ <button class="btn btn-outline-dark mr-2" *ngIf="matchSetType=='biblio'"
+ (click)="setNewPointType('attr')" i18n>Record Attribute</button>
+ <button class="btn btn-outline-dark mr-2"
+ (click)="setNewPointType('marc')" i18n>MARC Tag and Subfield</button>
+ <button class="btn btn-outline-dark mr-2" *ngIf="matchSetType=='authority'"
+ (click)="setNewPointType('heading')" i18n>Normalized Authority Heading</button>
+ <button class="btn btn-outline-dark mr-2"
+ (click)="setNewPointType('bool')" i18n>Boolean Operator</button>
+ </div>
+ <div class="row ml-2 mt-4 p-2 border border-secondary" *ngIf="newPointType">
+ <div class="col-lg-12 common-form striped-odd form-validated">
+ <ng-container *ngIf="newPointType=='attr'">
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Record Attribute:</div>
+ <div class="col-lg-4">
+ <eg-combobox [entries]="bibAttrDefEntries"
+ [required]="true"
+ (onChange)="newRecordAttr=$event ? $event.id : ''"
+ placeholder="Record Attribute..." i18n-placeholder>
+ </eg-combobox>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="newPointType=='marc'">
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Tag:</div>
+ <div class="col-lg-2">
+ <input required type="text" class="form-control" [(ngModel)]="newMarcTag"/>
+ </div>
+ </div>
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Subfield ‡:</div>
+ <div class="col-lg-2">
+ <input required type="text" class="form-control" [(ngModel)]="newMarcSf"/>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="newPointType=='heading'">
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Normalized Heading:</div>
+ <div class="col-lg-2">
+ <input type="checkbox" class="form-check-input" checked disabled/>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="newPointType!='bool'">
+ <div class="row mb-1">
+ <div class="col-lg-3">Match Score:</div>
+ <div class="col-lg-2">
+ <input required type="number" class="form-control"
+ [(ngModel)]="newMatchScore" step="0.1"/>
+ </div>
+ </div>
+ <div class="row mb-1">
+ <div class="col-lg-3">Negate:</div>
+ <div class="col-lg-2">
+ <input type="checkbox"
+ class="form-check-input" [(ngModel)]="newNegate"/>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="newPointType=='bool'">
+ <div class="row mb-1">
+ <div class="col-lg-3">Operator:</div>
+ <div class="col-lg-2">
+ <select class="form-control" [(ngModel)]="newBoolOp">
+ <option value='AND' i18n>AND</option>
+ <option value='OR' i18n>OR</option>
+ </select>
+ </div>
+ </div>
+ </ng-container>
+ <div class="row mt-2">
+ <button class="btn btn-success" (click)="addChildNode()"
+ [disabled]="!selectedIsBool()" i18n>
+ Add To Selected Node
+ </button>
+ </div>
+ <div class="row mt-2 font-italic">
+ <ol i18n>
+ <li>Define a new match point using the above fields.</li>
+ <li>Select a boolean node in the tree.</li>
+ <li>Click the "Add..." button to add the new matchpoint
+ as a child of the selected node.</li>
+ </ol>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-5">
<ng-container *ngIf="tree">
<div class="d-flex">
<button class="btn btn-warning mr-1" (click)="deleteNode()"
- [disabled]="!hasActiveNode()" i18n>
+ [disabled]="!hasSelectedNode()" i18n>
Remove Selected Node
</button>
+ <button class="btn btn-success mr-1" (click)="saveTree()"
+ [disabled]="!changesMade" i18n>
+ Save Changes
+ </button>
</div>
<div class="pt-2">
<eg-tree
</eg-tree>
</div>
</ng-container>
+ </div>
</div>
import {Component, OnInit, ViewChild, AfterViewInit, Input} from '@angular/core';
-import {IdlObject} from '@eg/core/idl.service';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {OrgService} from '@eg/core/org.service';
import {Tree, TreeNode} from '@eg/share/tree/tree';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {StringService} from '@eg/share/string/string.service';
@Component({
selector: 'eg-match-set-expression',
@Input() set matchSet(ms: IdlObject) {
this.matchSet_ = ms;
if (ms && !this.initDone) {
+ this.matchSetType = ms.mtype();
this.initDone = true;
this.refreshTree();
}
tree: Tree;
initDone: boolean;
+ matchSetPoints: {[id: string]: IdlObject};
+ matchSetType: string;
+ changesMade: boolean;
+
+ // Current type of new match point
+ newPointType: string;
+ newRecordAttr: string;
+ newMatchScore: number;
+ newNegate: boolean;
+ newMarcTag: string;
+ newMarcSf: string;
+ newHeading: string;
+ newBoolOp: string;
+ newId: number;
+
+ bibAttrDefs: IdlObject[];
+ bibAttrDefEntries: ComboboxEntry[];
constructor(
+ private idl: IdlService,
private pcrud: PcrudService,
- private org: OrgService
- ) { }
+ private org: OrgService,
+ private strings: StringService
+ ) {
+ this.bibAttrDefs = [];
+ this.bibAttrDefEntries = [];
+ this.matchSetPoints = {};
+ this.newId = -1;
+ }
ngOnInit() {
+
+ this.pcrud.retrieveAll('crad', {order_by: {crad: 'label'}})
+ .subscribe(attr => {
+ this.bibAttrDefs.push(attr);
+ this.bibAttrDefEntries.push({id: attr.name(), label: attr.label()});
+ });
}
- refreshTree() {
+ refreshTree(): Promise<any> {
if (!this.matchSet_) { return; }
+ this.matchSetPoints = {};
- this.pcrud.search('vmsp',
- {match_set: this.matchSet_.id()}, {}, {atomic: true}
+ return this.pcrud.search('vmsp',
+ {match_set: this.matchSet_.id()}, {},
+ {atomic: true, authoritative: true}
).toPromise().then(points => {
// create tree nodes
const nodes = [];
const idmap: any = {};
points.forEach(point => {
+
+ // Track the from-database point objects
+ this.matchSetPoints[point.id()] = point;
+
+ point.negate(point.negate() === 't' ? true : false);
+ point.heading(point.heading() === 't' ? true : false);
+
const node = new TreeNode({
id: point.id(),
expanded: true,
- label: point.bool_op()
- || point.svf()
- || (point.tag() + ' ‡' + point.subfield())
+ callerData: {point: point}
});
- nodes.push(node);
idmap[node.id + ''] = node;
+ this.setNodeLabel(node, point).then(() => nodes.push(node));
});
// then apply the parent/child relationship
});
}
- nodeClicked(node: TreeNode) {
- console.log('node clicked: ' + node.label
- + ' expanded ' + node.expanded);
+ setNodeLabel(node: TreeNode, point: IdlObject): Promise<any> {
+ if (node.label) { return Promise.resolve(null); }
+ return Promise.all([
+ this.getPointLabel(point, true).then(txt => node.label = txt),
+ this.getPointLabel(point, false).then(
+ txt => node.callerData.slimLabel = txt)
+ ]);
+ }
+
+ getPointLabel(point: IdlObject, showmatch?: boolean): Promise<string> {
+ return this.strings.interpolate(
+ 'staff.cat.vandelay.matchpoint.label',
+ {point: point, showmatch: showmatch}
+ );
}
+ nodeClicked(node: TreeNode) {}
+
deleteNode() {
- this.tree.removeNode(this.tree.activeNode());
+ this.changesMade = true;
+ const node = this.tree.selectedNode()
+ const point = this.matchSetPoints[node.id];
+ if (point) {
+ // point won't be cached if it's new during this session.
+ point.isdeleted(true);
+ }
+ this.tree.removeNode(node);
+ }
+
+ hasSelectedNode(): boolean {
+ return Boolean(this.tree.selectedNode());
+ }
+
+ selectedIsBool(): boolean {
+ if (this.tree) {
+ const node = this.tree.selectedNode();
+ return node && node.callerData.point.bool_op();
+ }
+ return false;
+ }
+
+ addChildNode() {
+ this.changesMade = true;
+
+ const pnode = this.tree.selectedNode();
+ const point = this.idl.create('vmsp');
+ point.id(this.newId--);
+ point.isnew(true);
+ point.parent(pnode.id);
+ point.match_set(this.matchSet_.id());
+
+ if (this.newPointType === 'bool') {
+ point.bool_op(this.newBoolOp);
+
+ } else {
+
+ if (this.newPointType == 'attr') {
+ point.svf(this.newRecordAttr);
+
+ } else if (this.newPointType == 'marc') {
+ point.tag(this.newMarcTag);
+ point.subfield(this.newMarcSf ? this.newMarcSf : null);
+ } else if (this.newPointType == 'heading') {
+ point.heading(true);
+ }
+
+ point.negate(this.newNegate);
+ point.quality(this.newMatchScore);
+ }
+
+ const node: TreeNode = new TreeNode({
+ id: point.id(),
+ callerData: {point: point}
+ });
+
+ // Match points are added to the DB only when the tree is saved.
+ this.setNodeLabel(node, point).then(() => pnode.children.push(node));
+ }
+
+ setNewPointType(type_: string) {
+ this.newPointType = type_;
+ this.newRecordAttr = '';
+ this.newMatchScore = 1;
+ this.newNegate = false;
+ this.newMarcTag = '';
+ this.newMarcSf = '';
+ this.newBoolOp = 'AND';
+ }
+
+ expressionAsString(): string {
+ if (!this.tree) { return ''; }
+
+ const renderNode = (node: TreeNode): string => {
+ if (!node) { return ''; }
+
+ if (node.children.length) {
+ return '(' + node.children.map(renderNode).join(
+ ' ' + node.callerData.slimLabel + ' ') + ')'
+ } else if (!node.callerData.point.bool_op()) {
+ return node.callerData.slimLabel;
+ } else {
+ return '()';
+ }
+ }
+
+ return renderNode(this.tree.rootNode);
}
- hasActiveNode(): boolean {
- return Boolean(this.tree.activeNode());
+ saveTree(): Promise<any> {
+
+ // New nodes
+ let nodes = this.tree.nodeList()
+ .filter(node => node.callerData.point.isnew())
+ .map(node => node.callerData.point);
+
+ // Deleted nodes
+ nodes = nodes.concat(
+ Object.values(this.matchSetPoints)
+ .filter(point => point.isdeleted())
+ );
+
+ return this.pcrud.autoApply(nodes)
+ .toPromise().then(() => this.refreshTree())
}
}