| /******************************************************************************** |
| * Copyright (c) 2015-2018 Contributors to the Eclipse Foundation |
| * |
| * See the NOTICE file(s) distributed with this work for additional |
| * information regarding copyright ownership. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| ********************************************************************************/ |
| |
| |
| import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; |
| import { ActivatedRoute } from '@angular/router'; |
| import { DatePipe } from '@angular/common'; |
| |
| import { TreeNode } from 'primeng/api'; |
| import { TreeTable } from 'primeng/primeng'; |
| import { Observable } from 'rxjs'; |
| import { TranslateService } from '@ngx-translate/core'; |
| |
| import { Node, Attribute, ContextGroup } from '@navigator/node'; |
| import { NavigatorService } from '@navigator/navigator.service'; |
| import { MDMNotificationService } from '@core/mdm-notification.service'; |
| import { TRANSLATE } from '@core/mdm-core.module'; |
| import { FileService } from '@file-explorer/services/file.service'; |
| import { FileLinkContextWrapper } from '@file-explorer/model/file-explorer.model'; |
| import { AuthenticationService } from '../../../authentication/authentication.service'; |
| |
| import { Sensor, Components, Context, ContextAttributeIdentifier} from '../../model/details.model'; |
| import { ContextService } from '../../services/context.service'; |
| import { Role } from 'src/app/authentication/authentication.service'; |
| |
| @Component({ |
| selector: 'mdm-detail-context', |
| templateUrl: 'mdm-detail-descriptive-data.component.html', |
| styleUrls: ['mdm-detail-descriptive-data.component.css'], |
| }) |
| |
| export class MDMDescriptiveDataComponent implements OnInit { |
| |
| private readonly StatusLoading = TRANSLATE('details.mdm-detail-descriptive-data.status-loading'); |
| private readonly StatusSaving = TRANSLATE('details.mdm-detail-descriptive-data.status-saving'); |
| private readonly StatusNoNodes = TRANSLATE('details.mdm-detail-descriptive-data.status-no-nodes-available'); |
| private readonly StatusNoDescriptiveData = TRANSLATE('details.mdm-detail-descriptive-data.status-no-descriptive-data-available'); |
| |
| public treeNodes: {[key: string]: TreeNode[]}; |
| |
| // this holds the type dependant original context data in case of cancelling the edit mode |
| private tmpTreeNodes: Node[]; |
| |
| public selectedNode: Node; |
| public contextType: string; |
| |
| public contextComponents: Components; |
| public sensors: Sensor[]; |
| public status: string; |
| |
| // reference to be able to use enum in html template |
| public contextGroupEnum = ContextGroup; |
| |
| public editMode: boolean; |
| |
| public isTreeTableExpanded = true; |
| |
| constructor(private route: ActivatedRoute, |
| private _contextService: ContextService, |
| private navigatorService: NavigatorService, |
| private notificationService: MDMNotificationService, |
| private translateService: TranslateService, |
| private datePipe: DatePipe, |
| private authenticationService: AuthenticationService, |
| private fileService: FileService) { |
| } |
| |
| public canEdit: boolean; |
| |
| ngOnInit() { |
| this.authenticationService.isUserInRole([Role.Admin.toString(), Role.User.toString()]).subscribe(b => this.canEdit = b); |
| this.status = this.StatusLoading; |
| this.editMode = false; |
| |
| this.refreshDetailData(this.navigatorService.getSelectedNode()); |
| this.route.params |
| .subscribe( |
| params => this.refreshContextData(params['context']), |
| error => this.notificationService.notifyError( |
| this.translateService.instant('details.mdm-detail-descriptive-data.err-cannot-load-scope'), error)); |
| |
| // node changed from the navigation tree |
| this.navigatorService.selectedNodeChanged |
| .subscribe( |
| node => this.refreshDetailData(node), |
| error => this.notificationService.notifyError( |
| this.translateService.instant('details.mdm-detail-descriptive-data.err-cannot-load-data'), error)); |
| |
| // file changed |
| this.fileService.onFileChanged().subscribe(link => this.handleFileChanged(link)); |
| } |
| |
| handleFileChanged(fileLinkContextWrapper: FileLinkContextWrapper) { |
| if (this.treeNodes != undefined) { |
| this.treeNodes[fileLinkContextWrapper.contextType] |
| .filter(tnode => tnode.label === fileLinkContextWrapper.contextComponent.name) |
| .map(tnode => tnode.children) |
| .reduce((a, b) => a.concat(b), []) |
| .filter(tnode => tnode.data.attribute.name === fileLinkContextWrapper.attribute.name) |
| .forEach(tnode => tnode.data.attribute = fileLinkContextWrapper.attribute); |
| // spread is necessary for treeTable changeDetection |
| this.treeNodes[fileLinkContextWrapper.contextType] = [... |
| this.treeNodes[fileLinkContextWrapper.contextType]]; |
| } |
| } |
| |
| setContext(context: string) { |
| let contextType: string; |
| |
| switch (context) { |
| case 'uut': |
| contextType = 'UNITUNDERTEST'; |
| break; |
| case 'te': |
| contextType = 'TESTEQUIPMENT'; |
| break; |
| case 'ts': |
| contextType = 'TESTSEQUENCE'; |
| break; |
| } |
| this.contextType = contextType; |
| } |
| |
| /** |
| * Listener method to change the context tab |
| * |
| * @param contextType |
| */ |
| refreshContextData(contextType: string) { |
| // revert before changing the context |
| this.undoEditChanges(undefined, this.editMode); |
| this.setContext(contextType); |
| this.editMode = false; |
| } |
| |
| /** |
| * Listener method to change the node |
| * |
| * @param node |
| */ |
| refreshDetailData(node: Node) { |
| if (node != undefined) { |
| this.selectedNode = node; |
| this.status = this.StatusLoading; |
| this.editMode = false; |
| this.contextComponents = undefined; |
| this.treeNodes = undefined; |
| if (node.type.toLowerCase() === 'measurement' || node.type.toLowerCase() === 'teststep' || node.type.toLowerCase() === 'test') { |
| this.loadContext(node); |
| } else { |
| this.status = this.StatusNoDescriptiveData; |
| } |
| } else { |
| this.status = this.StatusNoNodes; |
| } |
| } |
| |
| /** |
| * Load the context data for the provided node |
| * |
| * @param node |
| */ |
| private loadContext(node: Node) { |
| this._contextService.getContext(node).subscribe( |
| components => { |
| if (components['UNITUNDERTEST'] != undefined |
| || components['TESTEQUIPMENT'] != undefined |
| || components['TESTSEQUENCE'] != undefined) { |
| this.contextComponents = components; |
| this.treeNodes = this.mapComponents(components); |
| } else { |
| this.status = this.StatusNoDescriptiveData; |
| } |
| }, |
| error => this.notificationService.notifyError( |
| this.translateService.instant('details.mdm-detail-descriptive-data.err-cannot-load-context'), error) |
| ); |
| } |
| |
| getIdentifier(contextDescribable: Node, contextComponent: Node, attribute: Attribute, contextGroup?: ContextGroup, contextType?: string) { |
| return new ContextAttributeIdentifier(contextDescribable, contextComponent, attribute, contextGroup, contextType); |
| } |
| |
| getNodeClass(item: Node) { |
| return 'icon ' + item.type.toLowerCase(); |
| } |
| |
| /** |
| * Change the tree state to expanded or collapsed |
| * |
| * @param type |
| * @param expand |
| */ |
| toggleTreeNodeState(tt: TreeTable) { |
| this.isTreeTableExpanded = !this.isTreeTableExpanded; |
| const nodeArray = this.treeNodes[this.contextType]; |
| if (nodeArray != undefined) { |
| for (let node of nodeArray) { |
| this.recursiveChangeNodes(node, this.isTreeTableExpanded); |
| } |
| } |
| |
| // invoke table update when pressing the plus or minus buttons from the table header |
| tt.updateSerializedValue(); |
| tt.tableService.onUIUpdate(tt.value); |
| } |
| |
| /** |
| * Change the tree node state recursively |
| * |
| * @param node |
| * @param expand |
| */ |
| recursiveChangeNodes(node: TreeNode, expand: boolean) { |
| node.expanded = expand; |
| if (node.children != undefined && node.children.length > 0) { |
| for (let child of node.children) { |
| this.recursiveChangeNodes(child, expand); |
| } |
| } |
| } |
| |
| /** |
| * Get the nodes from the current context |
| * |
| * @param nodes |
| * @param parentId optional parent id which will get the child nodes |
| */ |
| getNodes(nodes: Node[], parentId: string) { |
| return nodes.filter(n => this.nodeIsInCurrentContext(n, parentId)); |
| } |
| |
| nodeIsInCurrentContext(node: Node, parentId: string) { |
| let parentNodeId = node.relations != null && node.relations.length > 0 ? node.relations[0].parentId : null; |
| return parentId == null && parentNodeId == null |
| || parentId != null && parentNodeId != null && parentId === parentNodeId; |
| } |
| |
| /** |
| * Create a tree node based on the mdm entity |
| * |
| * @param node |
| * @param context |
| */ |
| createTreeNode(node: Node, children: Node[]) { |
| return <TreeNode>{ |
| label: node.name, |
| data: { |
| 'name': node.name, |
| 'attribute': node, |
| 'header': true |
| }, |
| children: this.createTreeChildren(node, children), |
| icon: this.getNodeClass(node), |
| expanded: true |
| }; |
| } |
| |
| /** |
| * Create the tree children nodes recursive based on the mdm attributes and subsequent mdm entities |
| * |
| * @param node the current node |
| * @param contexts the complete contexts |
| */ |
| createTreeChildren(node: Node, children: Node[]) { |
| let list = []; |
| |
| for (let attribute of node.attributes) { |
| let tmp = <TreeNode>{ |
| data: { |
| 'name': attribute.name, |
| 'attribute': this.patchAttributeForDate(attribute), |
| 'header': false |
| }, |
| expanded: true |
| }; |
| list.push(tmp); |
| } |
| |
| if (node.relations != null && node.relations.length > 0) { |
| for (let relation of node.relations) { |
| if (relation.ids != null && relation.ids.length > 0) { |
| for (let id of relation.ids) { |
| let nodes = this.getNodes(children, id); |
| for (let n of nodes) { |
| list.push(this.createTreeNode(n, children)); |
| } |
| } |
| } |
| } |
| } |
| return list; |
| } |
| |
| /** |
| * Method to create a tree structure from the flat context entity and attribute map |
| * |
| * @param components |
| */ |
| mapComponents(components: Components) { |
| const list: {[key: string]: TreeNode[]} = {}; |
| if (components != undefined ) { |
| for (let key of Object.keys(components)) { |
| const nodes: Node[] = this.getNodes(components[key], null); |
| if (nodes != undefined) { |
| list[key] = nodes.map(node => this.createTreeNode(node, components[key])); |
| } |
| } |
| } |
| return list; |
| } |
| |
| /** |
| * Convert the date for UI or backend |
| * |
| * @param attribute |
| */ |
| patchAttributeForDate(attribute: Attribute) { |
| if (attribute.dataType != null && attribute.dataType.length > 0 && 'DATE' === attribute.dataType) { |
| const val = attribute.value as any[]; |
| if (val != null && val.length > 0) { |
| if (val[0] != null && val[0].length > 0) { |
| val[0] = this.convertFixedDateStr(val[0], true); |
| } |
| if (val.length > 1 && val[1] != null && val[1].length > 0) { |
| val[1] = this.convertFixedDateStr(val[1], true); |
| } |
| } |
| } |
| return attribute; |
| } |
| |
| /** ********************* |
| * Edit functions start |
| ********************** */ |
| |
| convertFixedDateStr(dt: string, convertForUI: boolean) { |
| let newDt = dt; |
| let sourceFormat = ''; |
| |
| /** |
| * Get the translation immediately |
| */ |
| sourceFormat = this.translateService.instant('details.mdm-detail-descriptive-data.transform-dateformat'); |
| |
| if (dt != null && dt.length > 0 && convertForUI && dt.indexOf('T') > -1) { |
| // for display purpose |
| let tmpDt = this.parseISOString(dt); |
| newDt = this.datePipe.transform(tmpDt, sourceFormat, '+0000'); |
| } else if (dt != null && dt.length > 0 && !convertForUI && dt.indexOf('T') === -1) { |
| // re-convert UI date to server date for persistence |
| if (newDt.indexOf('-') === -1) { |
| // find the date patterns dd, MM and yyyy and grab the according index positions |
| let startDay = sourceFormat.indexOf('d'); |
| let endDay = sourceFormat.lastIndexOf('d'); |
| let startMonth = sourceFormat.indexOf('M'); |
| let endMonth = sourceFormat.lastIndexOf('M'); |
| let startYear = sourceFormat.indexOf('y'); |
| let endYear = sourceFormat.lastIndexOf('y'); |
| // manually attach the time as toISOString() will not properly transform the winter/summer time |
| newDt = dt.substring(startYear, endYear + 1) + '-' + dt.substring(startMonth, endMonth + 1) |
| + '-' + dt.substring(startDay, endDay + 1); |
| if (dt.indexOf(' ') > -1) { |
| // use the provided timestamp |
| newDt = newDt + 'T' + dt.substring(dt.indexOf(' ') + 1) + ':00Z'; |
| } else { |
| newDt = newDt + 'T00:00:00Z'; |
| } |
| if (newDt.length !== 20) { |
| newDt = ''; |
| } |
| } |
| } |
| return newDt; |
| } |
| |
| /** |
| * Fixed parsing for ISO date string |
| * |
| * @param s |
| */ |
| parseISOString(s: string) { |
| let b: any[] = s.split(/\D+/); |
| return new Date(Date.UTC(b[0], --b[1], b[2], b[3], b[4], b[5], b[6])); |
| } |
| |
| /** |
| * Send the changed data to the backend |
| * |
| * @param node |
| * @param type |
| */ |
| private putContext(node: Node, type: string) { |
| if (type == null) { |
| return; |
| } |
| this.status = this.StatusSaving; |
| this.treeNodes = undefined; |
| |
| const data = new Context(); |
| data.ordered = new Components(); |
| data.ordered[type] = this.getNodesWithUpdatedContent(this.contextComponents[type] as Node[], true); |
| |
| data.measured = new Components(); |
| data.measured[type] = this.getNodesWithUpdatedContent(this.contextComponents[type] as Node[], false); |
| |
| // clear for status display |
| this.contextComponents = undefined; |
| |
| this._contextService.putContext(node, data).subscribe( |
| components => { |
| if (components.hasOwnProperty('UNITUNDERTEST') |
| || components.hasOwnProperty('TESTEQUIPMENT') |
| || components.hasOwnProperty('TESTSEQUENCE')) { |
| this.contextComponents = components; |
| this.treeNodes = this.mapComponents(this.contextComponents); |
| } else { |
| this.status = this.StatusNoDescriptiveData; |
| } |
| }, |
| error => this.notificationService.notifyError( |
| this.translateService.instant('details.mdm-detail-descriptive-data.err-cannot-load-context'), error) |
| ); |
| } |
| |
| onEdit(event: Event) { |
| event.stopPropagation(); |
| this.editMode = true; |
| this.tmpTreeNodes = JSON.parse(JSON.stringify(this.contextComponents[this.contextType])); |
| } |
| |
| onCancelEdit(event: Event) { |
| event.stopPropagation(); |
| this.editMode = false; |
| this.isTreeTableExpanded = true; |
| this.undoEditChanges(this.contextType, true); |
| } |
| |
| onSaveChanges(event: Event) { |
| event.stopPropagation(); |
| this.editMode = false; |
| this.isTreeTableExpanded = true; |
| this.putContext(this.selectedNode, this.contextType); |
| this.tmpTreeNodes = undefined; |
| } |
| |
| /** |
| * Method to revert table changes back to the original state |
| * |
| * @param type |
| */ |
| undoEditChanges(type: string, editMode: boolean) { |
| this.refreshDetailData(this.navigatorService.getSelectedNode()); |
| // if (this.contextComponents != undefined && editMode && this.tmpTreeNodes != undefined) { |
| // if (type == undefined) { |
| // type = this.contextType; |
| // } |
| // // contexts is the origin, so we revert this back as the tree attributes are just references |
| // this.contextComponents[type] = this.tmpTreeNodes; |
| // // revert back |
| // this.treeNodes = this.mapComponents(this.contextComponents); |
| // this.tmpTreeNodes = null; |
| // } |
| } |
| |
| /** |
| * Get the updated nodes from the current context |
| * |
| * @param contextComponents |
| * @param type the context type |
| * @param ordered true if ordered data, false if measured data |
| */ |
| getNodesWithUpdatedContent(contextComponents: Node[], ordered: boolean) { |
| let list = []; |
| for (let component of contextComponents) { |
| let parentNodeId = component.relations != null && component.relations.length > 0 ? component.relations[0].parentId : null; |
| if (parentNodeId == null) { |
| let attrs = []; |
| for (let cAttribute of component.attributes) { |
| let attr = new Attribute(); |
| let addAttr = true; |
| attr.dataType = cAttribute.dataType; |
| attr.name = cAttribute.name; |
| attr.unit = cAttribute.unit; |
| attr.value = ''; |
| |
| if (ordered && cAttribute.value instanceof Array && cAttribute.value.length > 0) { |
| attr.value = cAttribute.value[0]; |
| } else if (!ordered && cAttribute.value instanceof Array && cAttribute.value.length > 1) { |
| attr.value = cAttribute.value[1]; |
| } |
| // lookup new value from treenodes |
| if (attr.dataType === 'BOOLEAN' && attr.value != null && attr.value.toString().length > 0) { |
| attr.value = attr.value.toString() === 'true' ? '1' : '0'; |
| } |
| |
| // BUG: don't add if non-string as this throws an parsing error in the middleware |
| if (attr.dataType !== 'STRING' && (attr.value == null || attr.value.toString().length === 0)) { |
| addAttr = false; |
| } |
| |
| if (addAttr && attr.dataType === 'DATE') { |
| attr.value = this.convertFixedDateStr(attr.value as string, false); |
| } |
| |
| if (addAttr) { |
| if (this.isAttributeValueModified(attr, component, ordered)) { |
| attrs.push(attr); |
| } |
| } |
| } |
| // un-merged list |
| if (attrs.length > 0) { |
| let c = JSON.parse(JSON.stringify(component)); |
| c.attributes = attrs; |
| list.push(c); |
| } |
| } |
| } |
| return list; |
| } |
| |
| private isAttributeValueModified(attr: Attribute, component: Node, ordered: boolean) { |
| for (let tmpNode of this.tmpTreeNodes) { |
| if (tmpNode.name === component.name) { |
| for (let attribute of tmpNode.attributes) { |
| if (attribute.name === attr.name && attribute.value instanceof Array) { |
| let orgValue = ordered ? attribute.value[0] : |
| attribute.value.length > 1 ? attribute.value[1] : undefined; |
| if (orgValue != undefined) { |
| if (attr.dataType === 'BOOLEAN') { |
| // server value = true or false, UI value = 1 or 0 |
| return attr.value === '0' && orgValue === 'true' |
| || attr.value === '1' && orgValue === 'false' |
| || attr.value !== '' && orgValue === ''; |
| } else { |
| // plain comparison |
| return attr.value !== orgValue; |
| } |
| } |
| return false; |
| } |
| } |
| } |
| } |
| } |
| |
| /** ********************* |
| * Edit functions end |
| ********************** */ |
| |
| } |