diff options
Diffstat (limited to 'catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute')
3 files changed, 416 insertions, 0 deletions
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html new file mode 100644 index 0000000000..7f271af4e1 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html @@ -0,0 +1,102 @@ +<!-- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + --> + +<div *ngIf="!attribute.hidden" class="dynamic-property-row nested-level-{{nestedLevel}}" [@fadeIn] + [ngClass]="{'selected': selectedAttributeId && selectedAttributeId === attribute.propertiesName, 'readonly': attribute.isDisabled ||attribute.isDeclared}" + [class.with-top-border]="attribute.isChildOfListOrMap" + (click)="onClickPropertyRow(attribute, $event)"> + <!-- LEFT CELL --> + <ng-container *ngIf="!isAttributeFEModel"> + <div class="table-cell" *ngIf="canBeDeclared" [ngClass]="{'filtered':attribute.name === attributeNameSearchText}" [class.round-checkbox]="attribute.isDeclared"> <!-- simple children of complex type [@checkEffect]="property.isDeclared"--> + <checkbox *ngIf="hasDeclareOption" [(checked)]="attribute.isSelected" [disabled]="attribute.isDisabled ||attribute.isDeclared || readonly" (checkedChange)="checkAttribute.emit(attribute.propertiesName)" ></checkbox> + <div class="inner-cell-div" tooltip="{{attribute.name}}"><span>{{attribute.name}}</span></div> + </div> + <div class="table-cell" *ngIf="!canBeDeclared && !attribute.isChildOfListOrMap"> + <div class="inner-cell-div" tooltip="{{attribute.name}}"><span>{{attribute.name}}</span></div> + </div> <!-- simple children of complex type within map or list --> + <div class="table-cell map-entry" *ngIf="attribute.isChildOfListOrMap && attribType == derivedAttributeType.MAP"><!-- map left cell --> + <dynamic-element #mapKeyInput + class="value-input" + pattern="validationUtils.getValidationPattern(string)" + [value]="attribute.mapKey" + type="string" + [name]="attribute.name" + (elementChanged)="mapKeyChanged.emit($event.value)" + [readonly]="readonly" + [testId]="'prop-key-' + attributeTestsId" + ></dynamic-element> + </div> + </ng-container> + <!-- RIGHT CELL OR FULL WIDTH CELL--> + <ng-container *ngIf="attribType == derivedAttributeType.SIMPLE || attribute.isDeclared || (attribute.isChildOfListOrMap && attribType == derivedAttributeType.MAP && attribute.schema.property.isSimpleType)"> + <div class="table-cell"> + <dynamic-element class="value-input" + pattern="validationUtils.getValidationPattern(property.type)" + [value]="attribute.isDeclared ? attribute.value : attribute.valueObj" + [type]="attribute.isDeclared ? 'string' : attribute.type" + [name]="attribute.name" + [path]="attribute.propertiesName" + (elementChanged)="onElementChanged($event)" + [readonly]="readonly || attribute.isDeclared || attribute.isDisabled" + [testId]="'prop-' + attributeTestsId" + [declared] = "attribute.isDeclared" + [constraints] = "constraints" + ></dynamic-element> + </div> + </ng-container> + <ng-container *ngIf="!isAttributeFEModel && attribType != derivedAttributeType.SIMPLE && !attribute.isDeclared"> <!-- right cell for complex elements, or list complex --> + <div class="table-cell" *ngIf="attribType == derivedAttributeType.COMPLEX">{{attribute.type | contentAfterLastDot }}</div> + <div class="table-cell" *ngIf="attribType == derivedAttributeType.MAP && !attribute.schema.property.isSimpleType">{{attribute.schema.property.type | contentAfterLastDot }}</div> + </ng-container> + <ng-container *ngIf="isAttributeFEModel && (attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && !attribute.isDeclared"><!-- empty, full-width table cell - for PropertyFEModel of type list or map --> + <div class="table-cell empty"></div> + </ng-container> + <!-- ICONS: add, delete, and expand --> + <ng-container *ngIf="!attribute.isDeclared"> + <a *ngIf="(attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && !attribute.isChildOfListOrMap" class="property-icon add-item" (click)="createNewChildProperty();" [ngClass]="{'disabled':readonly || preventInsertItem(attribute)}" [attr.data-tests-id]="'add-to-list-' + attributeTestsId">Add value to list</a> + <span *ngIf="attribute.isChildOfListOrMap" (click)="deleteItem.emit(attribute);" class="property-icon sprite-new delete-item-icon" [ngClass]="{'disabled':readonly}" [attr.data-tests-id]="'delete-from-list-' + attributeTestsId"></span> + <span *ngIf="!isAttributeFEModel && (attribType == derivedAttributeType.COMPLEX || ((attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && hasChildren))" (click)="expandChildById(attribPath)" class="property-icon sprite-new round-expand-icon" [class.open]="expandedChildId.indexOf(attribPath) == 0" [attr.data-tests-id]="'expand-' + attributeTestsId" ></span> + </ng-container> + +</div> +<!-- FLAT CHILDREN --> +<div class="flat-children-container" *ngIf="isAttributeFEModel && !attribute.isDeclared"> + <ng-container *ngFor="let prop of attribute.flattenedChildren | filterChildAttributes: expandedChildId; trackBy:prop?.propertiesName"> + <dynamic-property + [selectedAttributeId]="selectedAttributeId" + [hasDeclareOption]="hasDeclareOption" + [canBeDeclared]="hasDeclareOption && prop.canBeDeclared" + [attribute]="prop" + [rootAttribute]="rootAttribute || attribute" + [expandedChildId]="expandedChildId" + [attributeNameSearchText]="attributeNameSearchText" + [readonly]="readonly" + [hasChildren]="getHasChildren(prop)" + (propertyChanged)="childValueChanged(prop)" + (mapKeyChanged)="updateChildKeyInParent(prop, $event)" + (expandChild)="expandChildById($event)" + (deleteItem)="deleteListOrMapItem($event)" + (clickOnAttributeRow)="onClickPropertyRow($event)" + (checkAttribute)="checkedChange($event)" + (addChildAttribsToParent)="addChildProps($event, prop.propertiesName)" + > + </dynamic-property> + </ng-container> +</div> diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less new file mode 100644 index 0000000000..fd572b0f2d --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less @@ -0,0 +1,82 @@ +@import '../../../../../../assets/styles/variables.less'; +.flat-children-container { + .dynamic-property-row { + /*create nested left border classes for up to 10 levels of nesting*/ + .nested-border-loop(@i) when (@i > 0) { + @size: (@i - 1) *2; + &.nested-level-@{i} .table-cell:first-child { + border-left: ~"solid @{size}px #009fdb"; + } + .nested-border-loop(@i - 1) + } + .nested-border-loop(10); + } + dynamic-property { + &:first-child .dynamic-property-row.with-top-border { + border-top:solid 1px #d2d2d2; + } + &:not(:last-child) .dynamic-property-row { + border-bottom:solid 1px #d2d2d2; + } + } +} +.dynamic-property-row { + display:flex; + flex-direction:row; + align-items: stretch; + + &.readonly{ + background-color: @tlv_color_t; + cursor: auto; + } + //for the case that the parent is disabled but the child is enabled + &:not(.readonly){ + background-color: @main_color_p; + } + + .table-cell { + flex: 1; + padding:9px; + justify-content: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:first-child { + flex: 0 0 50%; + border-right:#d2d2d2 solid 1px; + &:only-of-type { + flex: 1 1 100%; + border-right:none; + } + } + &.empty { + height:40px; + } + } + .property-icon { + flex: 0 0 auto; + margin-right:10px; + align-self:center; + cursor:pointer; + } +} + +.filtered { + /deep/ .checkbox-label-content{ + background-color: yellow; + } +} +.inner-cell-div{ + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; + display: inline; + padding-left: 8px; +} +.error { + border: solid 1px @func_color_q; + color: @func_color_q; + outline: none; + box-sizing: border-box; +} diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts new file mode 100644 index 0000000000..39faac9283 --- /dev/null +++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts @@ -0,0 +1,232 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +import * as _ from "lodash"; +import {Component, EventEmitter, Input, Output, ViewChild} from "@angular/core"; +import {PROPERTY_TYPES} from 'app/utils'; +import {DataTypeService} from "../../../../services/data-type.service"; +import {animate, style, transition, trigger} from '@angular/animations'; +import {IUiElementChangeEvent} from "../../../ui/form-components/ui-element-base.component"; +import {DynamicElementComponent} from "../../../ui/dynamic-element/dynamic-element.component"; +import {DerivedAttributeType} from "../../../../../models/attributes-outputs/attribute-be-model"; +import {AttributeFEModel} from "app/models/attributes-outputs/attribute-fe-model"; +import {DerivedFEAttribute} from "../../../../../models/attributes-outputs/derived-fe-attribute"; +import {AttributesUtils} from "../../../../pages/attributes-outputs/services/attributes.utils"; + +@Component({ + selector: 'dynamic-property', + templateUrl: './dynamic-attribute.component.html', + styleUrls: ['./dynamic-attribute.component.less'], + animations: [trigger('fadeIn', [transition(':enter', [style({opacity: '0'}), animate('.7s ease-out', style({opacity: '1'}))])])] +}) +export class DynamicAttributeComponent { + + derivedAttributeType = DerivedAttributeType; + attribType: DerivedAttributeType; + attribPath: string; + isAttributeFEModel: boolean; + nestedLevel: number; + attributeTestsId: string; + constraints: string[]; + + @Input() canBeDeclared: boolean; + @Input() attribute: AttributeFEModel | DerivedFEAttribute; + @Input() expandedChildId: string; + @Input() selectedAttributeId: string; + @Input() attributeNameSearchText: string; + @Input() readonly: boolean; + @Input() hasChildren: boolean; + @Input() hasDeclareOption: boolean; + @Input() rootAttribute: AttributeFEModel; + + @Output('attributeChanged') emitter: EventEmitter<void> = new EventEmitter<void>(); + @Output() expandChild: EventEmitter<string> = new EventEmitter<string>(); + @Output() checkAttribute: EventEmitter<string> = new EventEmitter<string>(); + @Output() deleteItem: EventEmitter<string> = new EventEmitter<string>(); + @Output() clickOnAttributeRow: EventEmitter<AttributeFEModel | DerivedFEAttribute> = new EventEmitter<AttributeFEModel | DerivedFEAttribute>(); + @Output() mapKeyChanged: EventEmitter<string> = new EventEmitter<string>(); + @Output() addChildAttribsToParent: EventEmitter<Array<DerivedFEAttribute>> = new EventEmitter<Array<DerivedFEAttribute>>(); + + @ViewChild('mapKeyInput') public mapKeyInput: DynamicElementComponent; + + constructor(private attributesUtils: AttributesUtils, private dataTypeService: DataTypeService) { + } + + ngOnInit() { + this.isAttributeFEModel = this.attribute instanceof AttributeFEModel; + this.attribType = this.attribute.derivedDataType; + this.attribPath = (this.attribute instanceof AttributeFEModel) ? this.attribute.name : this.attribute.attributesName; + this.nestedLevel = (this.attribute.attributesName.match(/#/g) || []).length; + this.rootAttribute = (this.rootAttribute) ? this.rootAttribute : <AttributeFEModel>this.attribute; + this.attributeTestsId = this.getAttributeTestsId(); + + this.initConstraintsValues(); + + } + + initConstraintsValues() { + let primitiveProperties = ['string', 'integer', 'float', 'boolean']; + + if (this.attribute.constraints) { + this.constraints = this.attribute.constraints[0].validValues + } + + //Complex Type + else if (primitiveProperties.indexOf(this.rootAttribute.type) == -1 && primitiveProperties.indexOf(this.attribute.type) >= 0) { + this.constraints = this.dataTypeService.getConstraintsByParentTypeAndUniqueID(this.rootAttribute.type, this.attribute.name); + } else { + this.constraints = null; + } + + } + + onClickPropertyRow = (property, event) => { + // Because DynamicAttributeComponent is recursive second time the event is fire event.stopPropagation = undefined + event && event.stopPropagation && event.stopPropagation(); + this.clickOnAttributeRow.emit(property); + } + + expandChildById = (id: string) => { + this.expandedChildId = id; + this.expandChild.emit(id); + } + + checkedChange = (propName: string) => { + this.checkAttribute.emit(propName); + } + + getHasChildren = (property: DerivedFEAttribute): boolean => {// enter to this function only from base property (AttributeFEModel) and check for child property if it has children + return _.filter((<AttributeFEModel>this.attribute).flattenedChildren, (prop: DerivedFEAttribute) => { + return _.startsWith(prop.attributesName + '#', property.attributesName); + }).length > 1; + } + + getAttributeTestsId = () => { + return [this.rootAttribute.name].concat(this.rootAttribute.getParentNamesArray(this.attribute.attributesName, [], true)).join('.'); + }; + + onElementChanged = (event: IUiElementChangeEvent) => { + this.attribute.updateValueObj(event.value, event.isValid); + this.emitter.emit(); + }; + + createNewChildProperty = (): void => { + + let newProps: Array<DerivedFEAttribute> = this.attributesUtils.createListOrMapChildren(this.attribute, "", null); + this.attributesUtils.assignFlattenedChildrenValues(this.attribute.valueObj, [newProps[0]], this.attribute.attributesName); + if (this.attribute instanceof AttributeFEModel) { + this.addChildProps(newProps, this.attribute.name); + } else { + this.addChildAttribsToParent.emit(newProps); + } + } + + addChildProps = (newProps: Array<DerivedFEAttribute>, childPropName: string) => { + + if (this.attribute instanceof AttributeFEModel) { + let insertIndex: number = this.attribute.getIndexOfChild(childPropName) + this.attribute.getCountOfChildren(childPropName); //insert after parent prop and existing children + this.attribute.flattenedChildren.splice(insertIndex, 0, ...newProps); //using ES6 spread operator + this.expandChildById(newProps[0].attributesName); + + this.updateMapKeyValueOnMainParent(newProps); + this.emitter.emit(); + } + } + + updateMapKeyValueOnMainParent(childrenProps: Array<DerivedFEAttribute>) { + if (this.attribute instanceof AttributeFEModel) { + const attributeFEModel: AttributeFEModel = <AttributeFEModel>this.attribute; + //Update only if all this attributeFEModel parents has key name + if (attributeFEModel.getParentNamesArray(childrenProps[0].attributesName, []).indexOf('') === -1) { + angular.forEach(childrenProps, (prop: DerivedFEAttribute): void => { //Update parent AttributeFEModel with value for each child, including nested props + attributeFEModel.childPropUpdated(prop); + if (prop.isChildOfListOrMap && prop.mapKey !== undefined) { + attributeFEModel.childPropMapKeyUpdated(prop, prop.mapKey, true); + } + }, this); + //grab the cumulative value for the new item from parent AttributeFEModel and assign that value to DerivedFEProp[0] (which is the list or map parent with UUID of the set we just added) + let parentNames = (<AttributeFEModel>attributeFEModel).getParentNamesArray(childrenProps[0].attributesName, []); + childrenProps[0].valueObj = _.get(attributeFEModel.valueObj, parentNames.join('.'), null); + } + } + } + + childValueChanged = (property: DerivedFEAttribute) => { //value of child property changed + + if (this.attribute instanceof AttributeFEModel) { // will always be the case + if (this.attribute.getParentNamesArray(property.attributesName, []).indexOf('') === -1) {//If one of the parents is empty key -don't save + this.attribute.childPropUpdated(property); + this.emitter.emit(); + } + } + } + + deleteListOrMapItem = (item: DerivedFEAttribute) => { + if (this.attribute instanceof AttributeFEModel) { + this.removeValueFromParent(item); + this.attribute.flattenedChildren.splice(this.attribute.getIndexOfChild(item.attributesName), this.attribute.getCountOfChildren(item.attributesName)); + this.expandChildById(item.attributesName); + } + } + + removeValueFromParent = (item: DerivedFEAttribute) => { + if (this.attribute instanceof AttributeFEModel) { + let itemParent = (item.parentName == this.attribute.name) + ? this.attribute : this.attribute.flattenedChildren.find(prop => prop.attributesName == item.parentName); + if (!itemParent) { + return; + } + + if (item.derivedDataType == DerivedAttributeType.MAP) { + const oldKey = item.getActualMapKey(); + delete itemParent.valueObj[oldKey]; + if (itemParent instanceof AttributeFEModel) { + delete itemParent.valueObjValidation[oldKey]; + itemParent.valueObjIsValid = itemParent.calculateValueObjIsValid(); + } + this.attribute.childPropMapKeyUpdated(item, null); // remove map key + } else { + const itemIndex: number = this.attribute.flattenedChildren.filter(prop => prop.parentName == item.parentName).map(prop => prop.attributesName).indexOf(item.attributesName); + itemParent.valueObj.splice(itemIndex, 1); + if (itemParent instanceof AttributeFEModel) { + itemParent.valueObjValidation.splice(itemIndex, 1); + itemParent.valueObjIsValid = itemParent.calculateValueObjIsValid(); + } + } + if (itemParent instanceof AttributeFEModel) { //direct child + this.emitter.emit(); + } else { //nested child - need to update parent prop by getting flattened name (recurse through parents and replace map/list keys, etc) + this.childValueChanged(itemParent); + } + } + } + + updateChildKeyInParent(childProp: DerivedFEAttribute, newMapKey: string) { + if (this.attribute instanceof AttributeFEModel) { + this.attribute.childPropMapKeyUpdated(childProp, newMapKey); + this.emitter.emit(); + } + } + + preventInsertItem = (property: DerivedFEAttribute): boolean => { + return property.type == PROPERTY_TYPES.MAP && Object.keys(property.valueObj).indexOf('') > -1; + } + +} |