diff options
Diffstat (limited to 'catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property')
4 files changed, 491 insertions, 0 deletions
diff --git a/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.html b/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.html new file mode 100644 index 0000000000..af72e6d6d6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.html @@ -0,0 +1,66 @@ +<div class="add-property-container"> + <loader [display]="isLoading" [size]="'large'" [relative]="true" [loaderDelay]="500"></loader> + <form class="w-sdc-form" [formGroup]="formGroup"> + + <div class="side-by-side"> + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label required">{{'PROPERTY_NAME_LABEL' | translate}}</label> + <input class="i-sdc-form-input" + type="text" + data-tests-id="property-name" + formControlName="name" + [readOnly]="readOnly"/> + </div> + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label required">{{'PROPERTY_TYPE_LABEL' | translate}}</label> + <select formControlName="type" (change)="onTypeChange()" [attr.disabled]="readOnly ? readOnly : null"> + <option [ngValue]="null">{{'GENERAL_LABEL_SELECT' | translate}}</option> + <option *ngFor="let type of typeList" + [ngValue]="type">{{type}}</option> + </select> + </div> + <div class="i-sdc-form-item"> + <input type="checkbox" formControlName="required" [attr.disabled]="readOnly ? readOnly : null"/> {{'PROPERTY_REQUIRED_LABEL' | translate}} + </div> + <div class="i-sdc-form-item propertySchemaType" *ngIf="showSchema"> + <label class="i-sdc-form-label required">{{'PROPERTY_SCHEMA_LABEL' | translate}}</label> + <select formControlName="schema" (change)="onSchemaChange()" [attr.disabled]="readOnly ? readOnly : null"> + <option [ngValue]="null">{{'GENERAL_LABEL_SELECT' | translate}}</option> + <option *ngFor="let type of schemaTypeList" + [ngValue]="type">{{type}}</option> + </select> + </div> + </div> + + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label">{{'PROPERTY_DESCRIPTION_LABEL' | translate}}</label> + <textarea class="i-sdc-form-textarea" + formControlName="description" + data-tests-id="property-description" + [readOnly]="readOnly"> + </textarea> + </div> + <div class="default-value-container i-sdc-form-item" *ngIf="showDefaultValue()"> + <label class="i-sdc-form-label">{{'PROPERTY_DEFAULT_VALUE_LABEL' | translate}}</label> + <ng-container *ngIf="!readOnly"> + <input type="checkbox" formControlName="hasDefaultValue" [attr.disabled]="readOnly ? readOnly : null"/> {{'PROPERTY_SET_DEFAULT_VALUE_MSG' | translate}} + </ng-container> + <ng-container *ngIf="hasDefaultValueForm.value"> + <ul> + <app-input-list-item + [name]="nameForm.value" + [type]="getDataType(typeForm.value)" + [dataTypeMap]="dataTypeMap" + [valueObjRef]="defaultValueForm.value" + [schema]="buildSchemaGroupProperty()" + [nestingLevel]="0" + [isViewOnly]="readOnly" + [allowDeletion]="false" + [isExpanded]="true" + (onValueChange)="onPropertyValueChange($event)"> + </app-input-list-item> + </ul> + </ng-container> + </div> + </form> +</div> diff --git a/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.less b/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.less new file mode 100644 index 0000000000..2c3300237c --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.less @@ -0,0 +1,104 @@ +/* + * - + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +.add-property-container { + max-width: 100%; +} + +.default-value-container { + overflow: scroll; + max-height: 300px; + max-width: 100%; + + ul { + margin: 0 0 0 20px; + list-style: none; + line-height: 2em; + } + + &::-webkit-scrollbar-track { + border: 0; + } +} + +li { + position: relative; + + &:before { + position: absolute; + left: -15px; + top: 0; + content: ''; + display: block; + border-left: 1px solid #ddd; + height: 1em; + border-bottom: 1px solid #ddd; + width: 10px; + } + + &:after { + position: absolute; + left: -15px; + bottom: -7px; + content: ''; + display: block; + border-left: 1px solid #ddd; + height: 100%; + } + + &.root { + margin: 0 0 0 -20px; + + &:before { + display: none; + } + + &:after { + display: none; + } + } + + &:last-child { + &:after { + display: none + } + } +} + +.input-label { + margin: 0; + font-weight: bold; +} + +.input-value { + display: flex; + flex-flow: row nowrap; + gap: 7px; + + input { + min-width: 150px; + max-width: 250px; + } +} + +.empty-value { + color: #aaaaaa; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.spec.ts b/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.spec.ts new file mode 100644 index 0000000000..7e2c312792 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.spec.ts @@ -0,0 +1,88 @@ +/* + * - + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; + +import {AddPropertyComponent} from './add-property.component'; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; +import {TranslateModule} from "../../../../shared/translator/translate.module"; +import {UiElementsModule} from "../../../../components/ui/ui-elements.module"; +import {Component, Input} from "@angular/core"; +import {DataTypeModel} from "../../../../../models/data-types"; +import {SchemaPropertyGroupModel} from "../../../../../models/schema-property"; +import {DataTypeService} from "../../../../services/data-type.service"; +import {TranslateService} from "../../../../shared/translator/translate.service"; + +@Component({selector: 'app-input-list-item', template: ''}) +class InputListItemStubComponent { + @Input() valueObjRef: any; + @Input() name: string; + @Input() dataTypeMap: Map<string, DataTypeModel>; + @Input() type: DataTypeModel; + @Input() schema: SchemaPropertyGroupModel; + @Input() nestingLevel: number; + @Input() isExpanded: boolean = false; + @Input() isListChild: boolean = false; + @Input() isMapChild: boolean = false; + @Input() listIndex: number; + @Input() isViewOnly: boolean; + @Input() allowDeletion: boolean = false; +} + +describe('AddPropertyComponent', () => { + let dataTypeServiceMock: Partial<DataTypeService> = { + findAllDataTypesByModel: jest.fn(args => { + return Promise.resolve(new Map()); + }) + }; + let translateServiceMock: Partial<TranslateService> = { + translate: jest.fn((str: string) => {}) + }; + let component: AddPropertyComponent; + let fixture: ComponentFixture<AddPropertyComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [AddPropertyComponent, InputListItemStubComponent], + imports: [ + FormsModule, + ReactiveFormsModule, + TranslateModule, + UiElementsModule + ], + providers: [ + {provide: DataTypeService, useValue: dataTypeServiceMock}, + {provide: TranslateService, useValue: translateServiceMock} + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddPropertyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.ts b/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.ts new file mode 100644 index 0000000000..dc1a0329ed --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/type-workspace/type-workspace-properties/add-property/add-property.component.ts @@ -0,0 +1,233 @@ +/* + * - + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; +import {PropertyBEModel} from "../../../../../models/properties-inputs/property-be-model"; +import {AbstractControl, FormControl, FormGroup, ValidationErrors, Validators} from "@angular/forms"; +import {PROPERTY_DATA} from "../../../../../utils/constants"; +import {DataTypeService} from "../../../../services/data-type.service"; +import {DataTypeModel} from "../../../../../models/data-types"; +import {Subscription} from "rxjs"; +import {ToscaTypeHelper} from "../../../../../utils/tosca-type-helper"; +import {SchemaProperty, SchemaPropertyGroupModel} from "../../../../../models/schema-property"; + +@Component({ + selector: 'app-add-property', + templateUrl: './add-property.component.html', + styleUrls: ['./add-property.component.less'] +}) +export class AddPropertyComponent implements OnInit, OnDestroy { + + @Input() property: PropertyBEModel; + @Input() readOnly: boolean = true; + @Input() model: string; + + @Output() onValidityChange: EventEmitter<PropertyValidationEvent> = new EventEmitter<PropertyValidationEvent>(); + + private valueChangesSub: Subscription; + private descriptionForm: FormControl = new FormControl(undefined); + private requiredForm: FormControl = new FormControl(false, Validators.required); + nameForm: FormControl = new FormControl(undefined, [Validators.required]); + typeForm: FormControl = new FormControl(undefined, Validators.required); + schemaForm: FormControl = new FormControl(undefined, (control: AbstractControl): ValidationErrors | null => { + if (this.typeNeedsSchema() && !control.value) { + return {required: true}; + } + return null; + }); + hasDefaultValueForm: FormControl = new FormControl(false, Validators.required); + defaultValueForm: FormControl = new FormControl(undefined); + formGroup: FormGroup = new FormGroup({ + 'name': this.nameForm, + 'description': this.descriptionForm, + 'type': this.typeForm, + 'required': this.requiredForm, + 'schema': this.schemaForm, + 'defaultValue': this.defaultValueForm, + 'hasDefaultValue': this.hasDefaultValueForm, + }); + + isLoading: boolean = false; + showSchema: boolean = false; + typeList: string[]; + dataTypeMap: Map<string, DataTypeModel>; + dataType: DataTypeModel; + schemaTypeList: string[]; + + constructor(private dataTypeService: DataTypeService) { + } + + ngOnInit(): void { + this.isLoading = true; + this.initTypeAndSchemaDropdown().then(() => this.updateDataType()); + this.initForm(); + this.valueChangesSub = this.formGroup.valueChanges.subscribe(() => { + this.emitValidityChange(); + }); + } + + ngOnDestroy(): void { + if (this.valueChangesSub) { + this.valueChangesSub.unsubscribe(); + } + } + + onSchemaChange(): void { + this.resetDefaultValue(); + } + + onTypeChange(): void { + this.schemaForm.setValue(null); + this.showSchema = this.typeNeedsSchema(); + this.updateDataType(); + this.resetDefaultValue(); + } + + private updateDataType(): void { + this.dataType = this.dataTypeMap.get(this.typeForm.value); + } + + private initForm(): void { + if (!this.property) { + return; + } + + this.nameForm.setValue(this.property.name); + this.descriptionForm.setValue(this.property.description); + this.typeForm.setValue(this.property.type); + this.showSchema = this.typeNeedsSchema(); + this.requiredForm.setValue(this.property.required); + this.schemaForm.setValue(this.property.schemaType); + this.initDefaultValueForm(); + } + + private initDefaultValueForm() { + if (this.property.defaultValue == undefined) { + return; + } + let defaultValue; + if (!this.isTypeSimple() && typeof this.property.defaultValue === 'string') { + defaultValue = JSON.parse(this.property.defaultValue); + } else { + defaultValue = this.property.defaultValue; + } + this.defaultValueForm.setValue(defaultValue); + this.hasDefaultValueForm.setValue(true); + } + + private typeNeedsSchema() { + return PROPERTY_DATA.SCHEMA_TYPES.indexOf(this.typeForm.value) > -1; + } + + private initTypeAndSchemaDropdown(): Promise<Map<string, DataTypeModel>> { + const primitiveTypes: string[] = Array.from(PROPERTY_DATA.TYPES).sort((a, b) => a.localeCompare(b)); + const promise = this.dataTypeService.findAllDataTypesByModel(this.model); + promise.then((dataTypeMap: Map<string, DataTypeModel>) => { + this.dataTypeMap = dataTypeMap; + const nonPrimitiveTypes: string[] = Array.from(dataTypeMap.keys()).filter(type => { + return primitiveTypes.indexOf(type) === -1; + }); + nonPrimitiveTypes.sort((a, b) => a.localeCompare(b)); + this.typeList = [...primitiveTypes, ...nonPrimitiveTypes]; + this.schemaTypeList = Array.from(this.typeList); + this.isLoading = false; + }); + return promise; + } + + private emitValidityChange(): void { + const isValid: boolean = this.formGroup.valid; + this.onValidityChange.emit({ + isValid: isValid, + property: isValid ? this.buildPropertyFromForm() : undefined + }); + } + + private buildPropertyFromForm(): PropertyBEModel { + const property = new PropertyBEModel(); + property.name = this.nameForm.value; + property.type = this.typeForm.value; + if (this.schemaForm.value) { + property.schemaType = this.schemaForm.value; + } + property.description = this.descriptionForm.value; + if (this.hasDefaultValueForm.value === true) { + property.defaultValue = this.defaultValueForm.value; + } + return property; + } + + public isTypeSimple(): boolean { + return ToscaTypeHelper.isTypeSimple(this.typeForm.value); + } + + public isTypeList(): boolean { + return ToscaTypeHelper.isTypeList(this.typeForm.value); + } + + public isTypeMap(): boolean { + return ToscaTypeHelper.isTypeMap(this.typeForm.value); + } + + public isTypeComplex(): boolean { + return ToscaTypeHelper.isTypeComplex(this.typeForm.value); + } + + private isTypeRange() { + return ToscaTypeHelper.isTypeRange(this.typeForm.value); + } + + onPropertyValueChange($event: any): void { + this.defaultValueForm.setValue($event.value); + } + + showDefaultValue(): boolean { + if (this.readOnly) { + return this.defaultValueForm.value != undefined && this.dataTypeMap && this.typeForm.valid && this.schemaForm.valid; + } + return this.dataTypeMap && this.typeForm.valid && this.schemaForm.valid; + } + + getDataType(type: string): DataTypeModel { + return this.dataTypeMap.get(type); + } + + private resetDefaultValue(): void { + this.defaultValueForm.reset(); + if (this.isTypeComplex() || this.isTypeMap()) { + this.defaultValueForm.setValue({}); + } else if (this.isTypeList() || this.isTypeRange()) { + this.defaultValueForm.setValue([]); + } + } + + buildSchemaGroupProperty(): SchemaPropertyGroupModel { + const schemaProperty = new SchemaProperty(); + schemaProperty.type = this.schemaForm.value + return new SchemaPropertyGroupModel(schemaProperty); + } + +} + +export class PropertyValidationEvent { + isValid: boolean; + property: PropertyBEModel; +}
\ No newline at end of file |