summaryrefslogtreecommitdiffstats
path: root/public/src/app/rule-engine/target
diff options
context:
space:
mode:
Diffstat (limited to 'public/src/app/rule-engine/target')
-rw-r--r--public/src/app/rule-engine/target/target.component.html28
-rw-r--r--public/src/app/rule-engine/target/target.component.scss99
-rw-r--r--public/src/app/rule-engine/target/target.component.spec.ts57
-rw-r--r--public/src/app/rule-engine/target/target.component.ts77
-rw-r--r--public/src/app/rule-engine/target/target.util.ts50
-rw-r--r--public/src/app/rule-engine/target/target.validation.spec.ts83
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);
+ });
+});