1 import {Component, OnInit, ViewChild, AfterViewInit, Input} from '@angular/core';
2 import {IdlObject, IdlService} from '@eg/core/idl.service';
3 import {PcrudService} from '@eg/core/pcrud.service';
4 import {NetService} from '@eg/core/net.service';
5 import {AuthService} from '@eg/core/auth.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {Tree, TreeNode} from '@eg/share/tree/tree';
8 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
9 import {StringService} from '@eg/share/string/string.service';
10 import {MatchSetNewPointComponent} from './match-set-new-point.component';
13 selector: 'eg-match-set-expression',
14 templateUrl: 'match-set-expression.component.html'
16 export class MatchSetExpressionComponent implements OnInit {
18 // Match set arrives from parent async.
20 @Input() set matchSet(ms: IdlObject) {
22 if (ms && !this.initDone) {
23 this.matchSetType = ms.mtype();
34 // Current type of new match point
38 @ViewChild('newPoint', { static: true }) newPoint: MatchSetNewPointComponent;
41 private idl: IdlService,
42 private pcrud: PcrudService,
43 private net: NetService,
44 private auth: AuthService,
45 private org: OrgService,
46 private strings: StringService
53 refreshTree(): Promise<any> {
54 if (!this.matchSet_) { return Promise.resolve(); }
56 return this.pcrud.search('vmsp',
57 {match_set: this.matchSet_.id()}, {},
58 {atomic: true, authoritative: true}
59 ).toPromise().then(points => {
60 if (points.length > 0) {
61 this.ingestMatchPoints(points);
68 // When creating a new tree, add a stub boolean node
69 // as the root so the tree has something to render.
72 const point = this.idl.create('vmsp');
73 point.id(this.newId--);
75 point.match_set(this.matchSet_.id());
79 const node: TreeNode = new TreeNode({
81 callerData: {point: point}
84 this.tree = new Tree(node);
85 this.setNodeLabel(node, point);
88 // Tree-ify a set of match points.
89 ingestMatchPoints(points: IdlObject[]) {
91 const idmap: any = {};
93 // massage data, create tree nodes
94 points.forEach(point => {
96 point.negate(point.negate() === 't' ? true : false);
97 point.heading(point.heading() === 't' ? true : false);
100 const node = new TreeNode({
103 callerData: {point: point}
105 idmap[node.id + ''] = node;
106 this.setNodeLabel(node, point).then(() => nodes.push(node));
109 // apply the tree parent/child relationships
110 points.forEach(point => {
111 const node = idmap[point.id() + ''];
112 if (point.parent()) {
113 idmap[point.parent() + ''].children.push(node);
115 this.tree = new Tree(node);
120 setNodeLabel(node: TreeNode, point: IdlObject): Promise<any> {
121 if (node.label) { return Promise.resolve(null); }
123 this.getPointLabel(point, true).then(txt => node.label = txt),
124 this.getPointLabel(point, false).then(
125 txt => node.callerData.slimLabel = txt)
129 getPointLabel(point: IdlObject, showmatch?: boolean): Promise<string> {
130 return this.strings.interpolate(
131 'staff.cat.vandelay.matchpoint.label',
132 {point: point, showmatch: showmatch}
136 nodeClicked(node: TreeNode) {}
139 this.changesMade = true;
140 const node = this.tree.selectedNode();
141 this.tree.removeNode(node);
144 hasSelectedNode(): boolean {
145 return Boolean(this.tree.selectedNode());
148 isRootNode(): boolean {
149 const node = this.tree.selectedNode();
150 if (node && this.tree.findParentNode(node) === null) {
156 selectedIsBool(): boolean {
158 const node = this.tree.selectedNode();
159 return node && node.callerData.point.bool_op();
164 addChildNode(replace?: boolean) {
165 this.changesMade = true;
167 const targetNode: TreeNode = this.tree.selectedNode();
171 point = targetNode.callerData.point;
172 point.ischanged(true);
173 // Clear previous data
174 ['bool_op', 'svf', 'tag', 'subfield', 'heading']
175 .forEach(f => point[f](null));
178 point = this.idl.create('vmsp');
179 point.id(this.newId--);
181 point.parent(targetNode.id);
182 point.match_set(this.matchSet_.id());
186 const ptype = this.newPoint.values.pointType;
188 if (ptype === 'bool') {
189 point.bool_op(this.newPoint.values.boolOp);
193 if (ptype === 'attr') {
194 point.svf(this.newPoint.values.recordAttr);
196 } else if (ptype === 'marc') {
197 point.tag(this.newPoint.values.marcTag);
198 point.subfield(this.newPoint.values.marcSf);
199 } else if (ptype === 'heading') {
203 point.negate(this.newPoint.values.negate);
204 point.quality(this.newPoint.values.matchScore);
209 targetNode.label = null;
210 this.setNodeLabel(targetNode, point);
214 const node: TreeNode = new TreeNode({
216 callerData: {point: point}
219 // Match points are added to the DB only when the tree is saved.
220 this.setNodeLabel(node, point)
221 .then(() => targetNode.children.push(node));
225 expressionAsString(): string {
226 if (!this.tree) { return ''; }
228 const renderNode = (node: TreeNode): string => {
229 if (!node) { return ''; }
231 if (node.children.length) {
232 return '(' + node.children.map(renderNode).join(
233 ' ' + node.callerData.slimLabel + ' ') + ')';
234 } else if (!node.callerData.point.bool_op()) {
235 return node.callerData.slimLabel;
241 return renderNode(this.tree.rootNode);
244 // Server API deletes and recreates the tree on update.
245 // It manages parent/child relationships via the children array.
246 // We only need send the current tree in a form the API recognizes.
247 saveTree(): Promise<any> {
250 const compileTree = (node?: TreeNode) => {
252 if (!node) { node = this.tree.rootNode; }
254 const point = node.callerData.point;
256 node.children.forEach(child =>
257 point.children().push(compileTree(child)));
262 const rootPoint: IdlObject = compileTree();
264 return this.net.request(
266 'open-ils.vandelay.match_set.update',
267 this.auth.token(), this.matchSet_.id(), rootPoint
269 ok => this.refreshTree(),
270 err => console.error(err)