diff options
Diffstat (limited to 'public/src/app/rule-engine/target')
6 files changed, 394 insertions, 0 deletions
diff --git a/public/src/app/rule-engine/target/target.component.html b/public/src/app/rule-engine/target/target.component.html new file mode 100644 index 0000000..7a321ef --- /dev/null +++ b/public/src/app/rule-engine/target/target.component.html @@ -0,0 +1,28 @@ +<form #targetFrm="ngForm" novalidate class="target"> + <div class="top-select"> + <span class="label" style="border-right: none;">Target</span> + <input class="text-input" style="border-right: none;" type="text" [(ngModel)]="selectedNode.id" (ngModelChange)="inputChange()" + ngModel required name="targetInput" data-tests-id="inputTarget"> + <span class="label clickable" data-tests-id="openTargetTree" style="border-left: none;" (click)="showOption = !showOption"> + <img src="{{imgBase}}/target.svg" alt="target"> + </span> + </div> + <div class="bottom-select" *ngIf="showOption" [@toggleDropdown]> + <div class="filter-container" style="display: flex; border-bottom: 1px solid #F2F2F2;margin-bottom: 1rem; width:100%;"> + <input id="filter" #filter class="filter" (keyup)="tree.treeModel.filterNodes(filter.value)" placeholder="Search..." /> + <button mat-raised-button style="min-width: 18px; box-shadow: none; display: flex; justify-content: center;" (click)="tree.treeModel.clearFilter(); filter.value = ''"> + <mat-icon>clear</mat-icon> + </button> + </div> + + <tree-root #tree [focused]="true" class="targetTree" (event)="onEvent($event)" [nodes]="nodes" [options]="options"> + <ng-template #treeNodeTemplate let-node let-index="index"> + <span *ngIf="node.data.isRequired" class="required"></span> + <span data-tests-id="targetNode"> + {{ node.data.name }} + </span> + </ng-template> + </tree-root> + + </div> +</form> diff --git a/public/src/app/rule-engine/target/target.component.scss b/public/src/app/rule-engine/target/target.component.scss new file mode 100644 index 0000000..ed2d70e --- /dev/null +++ b/public/src/app/rule-engine/target/target.component.scss @@ -0,0 +1,99 @@ +.targetTree { + tree-viewport { + overflow: hidden; + } +} + +.conatiner { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + justify-content: space-between; + + .center-content { + display: flex; + width: 100%; + + .action-info { + background: #93cdff; + padding: 6px; + border-radius: 5px; + height: 20px; + margin: 0 10px; + } + + .regex { + max-width: 250px; + float: right; + display: flex; + align-items: center; + padding: 20px 10px; + + .label { + border: 1px solid #d2d2d2; + padding: 0 5px; + height: 30px; + justify-content: center; + align-items: center; + display: flex; + } + } + } +} +.target { + width: 100%; + .top-select { + overflow: hidden; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + } + .label { + border: 1px solid #d2d2d2; + padding: 0 5px; + height: 30px; + justify-content: center; + align-items: center; + display: flex; + } + + .bottom-select { + border: 1px solid #ccc; + padding: 7px; + .filter-container { + padding: 5px; + .filter { + background: #fff; + color: black; + font: inherit; + border: 0; + outline: 0; + padding: 10px; + width: 100%; + } + } + } +} + +.small-padding { + padding-right: 10px; +} + +.text-input { + width: 100%; + height: 30px; + margin: 0; + padding: 0 5px; + border: 1px solid #d2d2d2; +} + +.clickable { + cursor: pointer; +} + +.required::before { + content: '*'; + color: red; +} diff --git a/public/src/app/rule-engine/target/target.component.spec.ts b/public/src/app/rule-engine/target/target.component.spec.ts new file mode 100644 index 0000000..6ddd8cd --- /dev/null +++ b/public/src/app/rule-engine/target/target.component.spec.ts @@ -0,0 +1,57 @@ +import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatButtonModule, MatIconModule } from '@angular/material'; +// component +import { TargetComponent } from './target.component'; + +describe('TargetComponent', () => { + let component: TargetComponent; + let fixture: ComponentFixture<TargetComponent>; + let de: DebugElement; + let el: HTMLElement; + + beforeEach( + async(() => { + TestBed.configureTestingModule({ + imports: [ + FormsModule, + BrowserAnimationsModule, + MatButtonModule, + MatIconModule + ], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + declarations: [TargetComponent] + }).compileComponents(); + }) + ); + + beforeEach(() => { + // create component and test fixture + fixture = TestBed.createComponent(TargetComponent); + // get test component from the fixture + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + it('should open target tree when click on button', () => { + const openTargetElement = fixture.debugElement + .query(By.css('span[data-tests-id=openTargetTree]')) + .nativeElement.click(); + + fixture.detectChanges(); + + const treeContainer = fixture.debugElement.query( + By.css('.filter-container') + ); + expect(treeContainer).not.toBeNull(); + }); +}); diff --git a/public/src/app/rule-engine/target/target.component.ts b/public/src/app/rule-engine/target/target.component.ts new file mode 100644 index 0000000..f17cdef --- /dev/null +++ b/public/src/app/rule-engine/target/target.component.ts @@ -0,0 +1,77 @@ +import { + Component, + ViewEncapsulation, + ViewChild, + Input, + Output, + EventEmitter +} from '@angular/core'; +import { TreeModel, TreeComponent, ITreeOptions } from 'angular-tree-component'; +import { + trigger, + state, + animate, + transition, + style +} from '@angular/animations'; +import { fuzzysearch, getBranchRequierds, validation } from './target.util'; +import { environment } from '../../../environments/environment'; +import { NgForm } from '@angular/forms'; + +@Component({ + selector: 'app-target', + templateUrl: './target.component.html', + styleUrls: ['./target.component.scss'], + encapsulation: ViewEncapsulation.None, + animations: [ + trigger('toggleDropdown', [ + transition('void => *', [ + style({ opacity: 0, offset: 0, height: 0 }), + animate('300ms cubic-bezier(0.17, 0.04, 0.03, 0.94)') + ]), + transition('* => void', [ + style({ opacity: 1, offset: 1, height: 'auto' }), + animate('100ms cubic-bezier(0.17, 0.04, 0.03, 0.94)') + ]) + ]) + ] +}) +export class TargetComponent { + imgBase = environment.imagePath; + showOption = false; + selectedNode = { name: '', id: '' }; + @Input() nodes; + @Output() onTargetChange = new EventEmitter(); + @ViewChild(TreeComponent) private tree: TreeComponent; + @ViewChild('targetFrm') targetFrm: NgForm; + options: ITreeOptions = { + animateExpand: true, + animateSpeed: 30, + animateAcceleration: 1.2 + }; + + filterFn(value, treeModel: TreeModel) { + treeModel.filterNodes(node => fuzzysearch(value, node.data.name)); + } + + inputChange() { + this.onTargetChange.emit(this.selectedNode.id); + } + + updateMode(action) { + this.selectedNode = { + id: action.target, + name: '' + }; + } + + onEvent(event) { + if (event.eventName === 'activate') { + if (event.node.data.children === null) { + this.selectedNode = event.node.data; + this.onTargetChange.emit(this.selectedNode); + this.showOption = false; + } + } + } +} diff --git a/public/src/app/rule-engine/target/target.util.ts b/public/src/app/rule-engine/target/target.util.ts new file mode 100644 index 0000000..6a6df62 --- /dev/null +++ b/public/src/app/rule-engine/target/target.util.ts @@ -0,0 +1,50 @@ +export function getBranchRequierds(node, requiredArr) { + if (node.parent) { + if (node.parent.data.hasOwnProperty('requiredChildren')) { + requiredArr.push(node.parent.data.requiredChildren); + } + return getBranchRequierds(node.parent, requiredArr); + } + return requiredArr; +} + +export function validation(node, userSelection) { + const requiredArr = []; + const validationRequired = getBranchRequierds(node, requiredArr); + const nonValidationArr = []; + validationRequired.forEach(nodeRequireds => { + return nodeRequireds.forEach(levelRequired => { + if (userSelection.filter(node => node === levelRequired).length === 0) { + nonValidationArr.push(levelRequired); + } + return; + }); + }); + return nonValidationArr; +} + +export function fuzzysearch(needle, haystack) { + const haystackLC = haystack.toLowerCase(); + const needleLC = needle.toLowerCase(); + + const hlen = haystack.length; + const nlen = needleLC.length; + + if (nlen > hlen) { + return false; + } + if (nlen === hlen) { + return needleLC === haystackLC; + } + outer: for (let i = 0, j = 0; i < nlen; i++) { + const nch = needleLC.charCodeAt(i); + + while (j < hlen) { + if (haystackLC.charCodeAt(j++) === nch) { + continue outer; + } + } + return false; + } + return true; +} diff --git a/public/src/app/rule-engine/target/target.validation.spec.ts b/public/src/app/rule-engine/target/target.validation.spec.ts new file mode 100644 index 0000000..71dc083 --- /dev/null +++ b/public/src/app/rule-engine/target/target.validation.spec.ts @@ -0,0 +1,83 @@ +import { TestBed, async } from '@angular/core/testing'; +import { TreeModel, TreeComponent, ITreeOptions } from 'angular-tree-component'; +import { validation, getBranchRequierds } from './target.util'; + +const _nodes = [ + { + id: 1, + name: 'North America', + requiredChildren: ['United States'], + children: [ + { + id: 11, + name: 'United States', + requiredChildren: ['New York', 'Florida'], + children: [ + { id: 111, name: 'New York' }, + { id: 112, name: 'California' }, + { id: 113, name: 'Florida' } + ] + }, + { id: 12, name: 'Canada' } + ] + }, + { + name: 'South America', + children: [{ name: 'Argentina', children: [] }, { name: 'Brazil' }] + }, + { + name: 'Europe', + children: [ + { name: 'England' }, + { name: 'Germany' }, + { name: 'France' }, + { name: 'Italy' }, + { name: 'Spain' } + ] + } +]; + +const tree = new TreeModel(); + +describe('treeTest', () => { + beforeAll(() => { + tree.setData({ + nodes: _nodes, + options: null, + events: null + }); + }); + + it('should return node branch requireds', () => { + // console.log('root', tree.getFirstRoot().data.name); + // console.log(tree.getNodeBy((node) => node.data.name === 'California').data.uuid); + // console.log(tree.getNodeBy((node) => node.data.name === 'California').id); + // console.log(tree.getNodeById(1)); + const selectedNode = tree.getNodeBy( + node => node.data.name === 'California' + ); + const result = getBranchRequierds(selectedNode, []); + const expected = [['New York', 'Florida'], ['United States']]; + + expect(result.length).toBeGreaterThan(1); + expect(result).toEqual(expected); + }); + + it('should return empty array - success state', () => { + const userSelect = ['Florida', 'New York', 'United States']; + const selectedNode = tree.getNodeBy(node => node.data.name === 'New York'); + const result = validation(selectedNode, userSelect); + + expect(result.length).toEqual(0); + expect(result).toEqual([]); + }); + + it('should return validation array - missing required filed', () => { + const userSelect = ['New York']; + const selectedNode = tree.getNodeBy(node => node.data.name === 'New York'); + const result = validation(selectedNode, userSelect); + const expected = ['Florida', 'United States']; + + expect(result).toEqual(expected); + }); +}); |