| ); | ); | ||||
| } | } | ||||
| /** | |||||
| * @param id | |||||
| * @param body | |||||
| * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. | |||||
| * @param reportProgress flag to report request and response progress. | |||||
| */ | |||||
| public update(id: string, body: object, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<object>; | |||||
| public update(id: string, body: object, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<object>>; | |||||
| public update(id: string, body: object, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<object>>; | |||||
| public update(id: string, body: object, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> { | |||||
| if (id === null || id === undefined) { | |||||
| throw new Error('Required parameter id was null or undefined when calling update.'); | |||||
| } | |||||
| if (body === null || body === undefined) { | |||||
| throw new Error('Required parameter body was null or undefined when calling update.'); | |||||
| } | |||||
| let headers = this.defaultHeaders; | |||||
| let credential: string | undefined; | |||||
| // authentication (basic) required | |||||
| credential = this.configuration.lookupCredential('basic'); | |||||
| if (credential) { | |||||
| headers = headers.set('Authorization', 'Basic ' + credential); | |||||
| } | |||||
| let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; | |||||
| if (httpHeaderAcceptSelected === undefined) { | |||||
| // to determine the Accept header | |||||
| const httpHeaderAccepts: string[] = [ | |||||
| 'application/json' | |||||
| ]; | |||||
| httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); | |||||
| } | |||||
| if (httpHeaderAcceptSelected !== undefined) { | |||||
| headers = headers.set('Accept', httpHeaderAcceptSelected); | |||||
| } | |||||
| // to determine the Content-Type header | |||||
| const consumes: string[] = [ | |||||
| 'application/json' | |||||
| ]; | |||||
| const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); | |||||
| if (httpContentTypeSelected !== undefined) { | |||||
| headers = headers.set('Content-Type', httpContentTypeSelected); | |||||
| } | |||||
| let responseType_: 'text' | 'json' = 'json'; | |||||
| if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { | |||||
| responseType_ = 'text'; | |||||
| } | |||||
| return this.httpClient.patch<object>(`${this.configuration.basePath}/rules/${encodeURIComponent(String(id))}`, | |||||
| body, | |||||
| { | |||||
| responseType: <any>responseType_, | |||||
| withCredentials: this.configuration.withCredentials, | |||||
| headers: headers, | |||||
| observe: observe, | |||||
| reportProgress: reportProgress | |||||
| } | |||||
| ); | |||||
| } | |||||
| } | } |
| */ | */ | ||||
| remove(id: string, extraHttpRequestParams?: any): Observable<{}>; | remove(id: string, extraHttpRequestParams?: any): Observable<{}>; | ||||
| /** | |||||
| * | |||||
| * | |||||
| * @param id | |||||
| * @param body | |||||
| */ | |||||
| update(id: string, body: object, extraHttpRequestParams?: any): Observable<object>; | |||||
| } | } |
| */ | */ | ||||
| text: string; | text: string; | ||||
| subRuleIds: Array<string>; | subRuleIds: Array<string>; | ||||
| parentId: string; | |||||
| } | } | ||||
| "@angular/platform-browser-dynamic": "~11.2.11", | "@angular/platform-browser-dynamic": "~11.2.11", | ||||
| "@angular/router": "~11.2.11", | "@angular/router": "~11.2.11", | ||||
| "@openapitools/openapi-generator-cli": "^2.2.6", | "@openapitools/openapi-generator-cli": "^2.2.6", | ||||
| "@types/base-64": "^1.0.0", | |||||
| "base-64": "^1.0.0", | |||||
| "rxjs": "~6.6.0", | "rxjs": "~6.6.0", | ||||
| "tslib": "^2.0.0", | "tslib": "^2.0.0", | ||||
| "zone.js": "~0.11.3" | "zone.js": "~0.11.3" | ||||
| "node": ">= 10.0.0" | "node": ">= 10.0.0" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/@types/base-64": { | |||||
| "version": "1.0.0", | |||||
| "resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.0.tgz", | |||||
| "integrity": "sha512-AvCJx/HrfYHmOQRFdVvgKMplXfzTUizmh0tz9GFTpDePWgCY4uoKll84zKlaRoeiYiCr7c9ZnqSTzkl0BUVD6g==" | |||||
| }, | |||||
| "node_modules/@types/component-emitter": { | "node_modules/@types/component-emitter": { | ||||
| "version": "1.2.10", | "version": "1.2.10", | ||||
| "dev": true, | "dev": true, | ||||
| "node": ">=0.10.0" | "node": ">=0.10.0" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/base-64": { | |||||
| "version": "1.0.0", | |||||
| "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", | |||||
| "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" | |||||
| }, | |||||
| "node_modules/base/node_modules/define-property": { | "node_modules/base/node_modules/define-property": { | ||||
| "version": "1.0.0", | "version": "1.0.0", | ||||
| "dev": true, | "dev": true, | ||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "@types/base-64": { | |||||
| "version": "1.0.0", | |||||
| "resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.0.tgz", | |||||
| "integrity": "sha512-AvCJx/HrfYHmOQRFdVvgKMplXfzTUizmh0tz9GFTpDePWgCY4uoKll84zKlaRoeiYiCr7c9ZnqSTzkl0BUVD6g==" | |||||
| }, | |||||
| "@types/component-emitter": { | "@types/component-emitter": { | ||||
| "version": "1.2.10", | "version": "1.2.10", | ||||
| "dev": true | "dev": true | ||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "base-64": { | |||||
| "version": "1.0.0", | |||||
| "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", | |||||
| "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" | |||||
| }, | |||||
| "base64-arraybuffer": { | "base64-arraybuffer": { | ||||
| "version": "0.1.4", | "version": "0.1.4", | ||||
| "dev": true | "dev": true |
| "@angular/platform-browser-dynamic": "~11.2.11", | "@angular/platform-browser-dynamic": "~11.2.11", | ||||
| "@angular/router": "~11.2.11", | "@angular/router": "~11.2.11", | ||||
| "@openapitools/openapi-generator-cli": "^2.2.6", | "@openapitools/openapi-generator-cli": "^2.2.6", | ||||
| "@types/base-64": "^1.0.0", | |||||
| "base-64": "^1.0.0", | |||||
| "rxjs": "~6.6.0", | "rxjs": "~6.6.0", | ||||
| "tslib": "^2.0.0", | "tslib": "^2.0.0", | ||||
| "zone.js": "~0.11.3" | "zone.js": "~0.11.3" |
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | import { CommonModule } from '@angular/common'; | ||||
| import { CreateRuleComponent } from './create-rule/create-rule.component'; | import { CreateRuleComponent } from './create-rule/create-rule.component'; | ||||
| import { MatCardModule } from '@angular/material/card'; | |||||
| import { MatFormFieldModule } from '@angular/material/form-field'; | |||||
| import { MatTreeModule } from '@angular/material/tree'; | |||||
| import { MatButtonModule } from '@angular/material/button'; | |||||
| import { MatIconModule } from '@angular/material/icon'; | |||||
| import { MatCommonModule } from '@angular/material/core'; | |||||
| import { MatInputModule } from '@angular/material/input'; | |||||
| import { DynamicDatabase } from './create-rule/class/dynamic-database'; | |||||
| @NgModule({ | @NgModule({ | ||||
| CreateRuleComponent | CreateRuleComponent | ||||
| ], | ], | ||||
| imports: [ | imports: [ | ||||
| CommonModule | |||||
| CommonModule, | |||||
| MatCardModule, | |||||
| MatFormFieldModule, | |||||
| MatTreeModule, | |||||
| MatButtonModule, | |||||
| MatIconModule, | |||||
| MatCommonModule, | |||||
| MatFormFieldModule, | |||||
| MatInputModule, | |||||
| ], | |||||
| providers: [ | |||||
| DynamicDatabase, | |||||
| ] | ] | ||||
| }) | }) | ||||
| export class AdminModule { } | export class AdminModule { } |
| import { CollectionViewer, DataSource, SelectionChange } from "@angular/cdk/collections"; | |||||
| import { FlatTreeControl } from "@angular/cdk/tree"; | |||||
| import { BehaviorSubject, merge, Observable } from "rxjs"; | |||||
| import { map } from "rxjs/operators"; | |||||
| import { DynamicDatabase } from "./dynamic-database"; | |||||
| import { RuleFlatNode } from "./rule-flat-node"; | |||||
| export class DynamicDataSource implements DataSource<RuleFlatNode> { | |||||
| dataChange = new BehaviorSubject<RuleFlatNode[]>([]); | |||||
| get data(): RuleFlatNode[] { return this.dataChange.value; } | |||||
| set data(value: RuleFlatNode[]) { | |||||
| this._treeControl.dataNodes = value; | |||||
| this.dataChange.next(value); | |||||
| } | |||||
| constructor(private _treeControl: FlatTreeControl<RuleFlatNode>, | |||||
| private _database: DynamicDatabase) { | |||||
| } | |||||
| connect(collectionViewer: CollectionViewer): Observable<RuleFlatNode[]> { | |||||
| this._treeControl.expansionModel.changed.subscribe(change => { | |||||
| if ((change as SelectionChange<RuleFlatNode>).added || | |||||
| (change as SelectionChange<RuleFlatNode>).removed) { | |||||
| this.handleTreeControl(change as SelectionChange<RuleFlatNode>); | |||||
| } | |||||
| }); | |||||
| return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data)); | |||||
| } | |||||
| disconnect(collectionViewer: CollectionViewer): void { } | |||||
| /** Handle expand/collapse behaviors */ | |||||
| handleTreeControl(change: SelectionChange<RuleFlatNode>) { | |||||
| if (change.added) { | |||||
| change.added.forEach(node => this.toggleNode(node, true)); | |||||
| } | |||||
| if (change.removed) { | |||||
| change.removed.slice().reverse().forEach(node => this.toggleNode(node, false)); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Toggle the node, remove from display list | |||||
| */ | |||||
| toggleNode(node: RuleFlatNode, expand: boolean) { | |||||
| const children = node.item.subRule; | |||||
| const index = this.data.indexOf(node); | |||||
| if (!children || index < 0) { // If no children, or cannot find the node, no op | |||||
| return; | |||||
| } | |||||
| setTimeout(() => { | |||||
| if (expand) { | |||||
| const nodes = children.map(name => | |||||
| new RuleFlatNode( | |||||
| name, | |||||
| node.level + 1, | |||||
| !!node.item.subRule ? node.item.subRule.length > 0 : false) | |||||
| ); | |||||
| this.data.splice(index + 1, 0, ...nodes); | |||||
| } else { | |||||
| let count = 0; | |||||
| for (let i = index + 1; i < this.data.length | |||||
| && this.data[i].level > node.level; i++, count++) { } | |||||
| this.data.splice(index + 1, count); | |||||
| } | |||||
| // notify the change | |||||
| this.dataChange.next(this.data); | |||||
| node.isLoading = false; | |||||
| }, 1000); | |||||
| } | |||||
| } |
| import { Injectable } from "@angular/core" | |||||
| import { RippleRef } from "@angular/material/core"; | |||||
| import { BehaviorSubject, Subject } from "rxjs"; | |||||
| import { map } from "rxjs/operators"; | |||||
| import { CreateRuleDto, Rule, RulesService } from "../../../../../api"; | |||||
| import { environment } from "../../../../environments/environment"; | |||||
| import { RuleFlatNode } from "./rule-flat-node"; | |||||
| @Injectable({ providedIn: 'root' }) | |||||
| export class DynamicDatabase { | |||||
| dataChange = new BehaviorSubject<Rule[]>([]); | |||||
| childAdded = new Subject<Rule>(); | |||||
| get data(): Rule[] { return this.dataChange.value; } | |||||
| get count(): number { return this.dataChange.value.length }; | |||||
| protected addData(rule: Rule) { this.dataChange.next([...this.dataChange.value, rule]) } | |||||
| constructor(protected rulesService: RulesService) { | |||||
| this.rulesService.configuration.basePath = environment.apiUrl; | |||||
| this.rulesService.findAll() | |||||
| .subscribe( | |||||
| (rules: Rule[]) => this.dataChange.next( | |||||
| rules | |||||
| .filter(this.filterRuleWithParentFromRootLayer) | |||||
| .sort(this.compareRulesByParagraph) | |||||
| ) | |||||
| ), | |||||
| (err) => { | |||||
| //TODO add toaster | |||||
| } | |||||
| } | |||||
| getParagraphOfNextChild = (parentRule): string => `${parentRule.paragraph}.${parentRule.subRule.length + 1}`; | |||||
| filterRuleWithParentFromRootLayer = (rootLayerRule: Rule) => typeof rootLayerRule.parentRule !== 'number'; | |||||
| compareRulesByParagraph = (ruleA: Rule, ruleB: Rule) => this.compareParahraph(ruleA.paragraph, ruleB.paragraph); | |||||
| compareParahraph(paragraphA: string, paragraphB: string): 0 | 1 | -1 { | |||||
| if (paragraphA === paragraphB) { | |||||
| return 0; | |||||
| } | |||||
| const paragrapASections = paragraphA.split('.'); | |||||
| const paragrapBSections = paragraphB.split('.'); | |||||
| if (+paragrapASections[0] > +paragrapBSections[0]) { | |||||
| return 1; | |||||
| } | |||||
| if (+paragrapASections[0] < +paragrapBSections[0]) { | |||||
| return -1; | |||||
| } | |||||
| if (+paragrapASections[0] === +paragrapBSections[0]) { | |||||
| let shiftedParagraphASections = paragrapASections; | |||||
| shiftedParagraphASections.shift(); | |||||
| let shiftedParagraphBSections = paragrapBSections; | |||||
| shiftedParagraphBSections.shift(); | |||||
| return this.compareParahraph( | |||||
| shiftedParagraphASections.join('.'), | |||||
| shiftedParagraphBSections.join('.'), | |||||
| ) | |||||
| } | |||||
| } | |||||
| getParentById = (ruleId: string) => this.data.find(rule => rule.id === ruleId) | |||||
| insertEmptyChild(parent: Rule) { | |||||
| parent.subRule.push( | |||||
| { | |||||
| text: '', | |||||
| id: '0', | |||||
| parentRule: parent, | |||||
| paragraph: this.getParagraphOfNextChild(parent) | |||||
| } | |||||
| ) | |||||
| this.dataChange.next(this.data); | |||||
| this.childAdded.next(parent); | |||||
| } | |||||
| insertEmptyParent() { | |||||
| this.addData({ | |||||
| text: '', | |||||
| id: '0', | |||||
| parentRule: null, | |||||
| paragraph: `${this.count + 2}` | |||||
| }) | |||||
| } | |||||
| insertChild(parent: Rule, subRule: Rule) { | |||||
| const createSubRule: CreateRuleDto = { | |||||
| paragraph: subRule.paragraph, | |||||
| text: subRule.text, | |||||
| subRuleIds: null, | |||||
| parentId: parent.id | |||||
| } | |||||
| this.rulesService.create(createSubRule).subscribe( | |||||
| newSubRule => { | |||||
| if (typeof newSubRule.id !== 'undefined' && typeof newSubRule.parentRule?.id !== 'undefined') { | |||||
| const updateParentRule: CreateRuleDto = { | |||||
| paragraph: parent.paragraph, | |||||
| text: parent.text, | |||||
| subRuleIds: [...parent.subRule ? parent.subRule.map(rule => rule.id) : [], newSubRule.id] | |||||
| } as CreateRuleDto; | |||||
| this.rulesService.update(parent.id, updateParentRule).subscribe((updated) => { | |||||
| //TODO Push updated message | |||||
| this.update(parent.id, { | |||||
| paragraph: parent.paragraph, | |||||
| subRuleIds: [ | |||||
| ...parent.subRule | |||||
| .filter(rule => rule.id === "0") | |||||
| .map(rule => rule.id) ?? [], | |||||
| subRule.id, | |||||
| ], | |||||
| text: parent.text | |||||
| }).subscribe( | |||||
| (updated) => { | |||||
| //todo add update message | |||||
| }, | |||||
| (err) => { | |||||
| //TODO Push error message | |||||
| console.error(err); | |||||
| } | |||||
| ) | |||||
| }, | |||||
| (err) => { | |||||
| //TODO Push error message | |||||
| console.error(err); | |||||
| } | |||||
| ) | |||||
| return; | |||||
| } | |||||
| //TODO Push error message | |||||
| } | |||||
| ) | |||||
| if (!parent.subRule) { | |||||
| parent.subRule = []; | |||||
| } | |||||
| const lastIndex = parent.subRule.indexOf(subRule); | |||||
| parent.subRule[lastIndex] = subRule; | |||||
| this.dataChange.next(this.data); | |||||
| this.childAdded.next(parent); | |||||
| } | |||||
| update(id: string, changedData: Partial<CreateRuleDto>) { | |||||
| return this.rulesService.update(id, changedData); | |||||
| } | |||||
| insertParent(newRule: Rule) { | |||||
| const createNewRule: CreateRuleDto = { | |||||
| paragraph: `${this.count + 1}`, | |||||
| text: newRule.text, | |||||
| subRuleIds: null, | |||||
| parentId: null | |||||
| } | |||||
| this.rulesService.create(createNewRule).subscribe( | |||||
| () => { | |||||
| //TODO created message | |||||
| }, | |||||
| (err) => { | |||||
| //TODO Error message | |||||
| } | |||||
| ) | |||||
| this.dataChange.next(this.data); | |||||
| } | |||||
| removeChild(parent: Rule, subRule: Rule) { | |||||
| if (parent.subRule) { | |||||
| const subRuleToRemove = this.findSubRuleFromParent(parent, subRule); | |||||
| const indexIfSubRule = parent.subRule.indexOf(subRuleToRemove); | |||||
| if (indexIfSubRule > -1) { | |||||
| parent.subRule.splice(indexIfSubRule, 1); | |||||
| } | |||||
| this.dataChange.next(this.data); | |||||
| } | |||||
| } | |||||
| findSubRuleFromParent = (parent: Rule, subRule: Rule): Rule | null => | |||||
| parent.subRule.find(rules => rules.paragraph === subRule.paragraph && rules.id === subRule.id); | |||||
| } |
| import { Rule } from "../../../../../api"; | |||||
| export class RuleFlatNode { | |||||
| constructor(public item: Rule, public level = 1, public expandable = false, | |||||
| public isLoading = false,public root = true) { } | |||||
| } | |||||
| <p>create-rule works!</p> | |||||
| <mat-card class="wood-card"> | |||||
| <mat-tree [dataSource]="dataSource" [treeControl]="treeControl"> | |||||
| <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding> | |||||
| <div> | |||||
| <button mat-icon-button disabled></button> §{{ node.item.paragraph }} {{ node.item.text }} | |||||
| <button *ngIf="node.root" mat-icon-button (click)="addEmptyRule(node)"> | |||||
| <mat-icon>add</mat-icon> | |||||
| </button> | |||||
| </div> | |||||
| </mat-tree-node> | |||||
| <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding> | |||||
| <button mat-icon-button matTreeNodeToggle [attr.aria-label]="'Toggle ' + node.text"> | |||||
| <mat-icon class="mat-icon-rtl-mirror"> | |||||
| {{ treeControl.isExpanded(node) ? "expand_more" : "chevron_right" }} | |||||
| </mat-icon> | |||||
| </button> §{{ node.item.paragraph }} {{ node.item.text }} | |||||
| <button *ngIf="node.root" mat-icon-button (click)="addEmptyRule(node)"> | |||||
| <mat-icon>add</mat-icon> | |||||
| </button> | |||||
| </mat-tree-node> | |||||
| <mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding> | |||||
| <button mat-icon-button disabled></button> | |||||
| <span>§{{ node.item.paragraph }}</span> | |||||
| <mat-form-field class="new-node-input"> | |||||
| <mat-label>New Rule</mat-label> | |||||
| <input [formControl]="text" matInput #itemValue placeholder="text" /> | |||||
| </mat-form-field> | |||||
| <button mat-button (click)="addNode(node, itemValue.value)">Save</button> | |||||
| </mat-tree-node> | |||||
| </mat-tree> | |||||
| <button mat-icon-button (click)="addEmptyRule()"> | |||||
| <mat-icon>add</mat-icon> | |||||
| </button> | |||||
| </mat-card> |
| mat-card { | |||||
| margin: 15px; | |||||
| color: white; | |||||
| } | |||||
| mat-tree-node { | |||||
| color: white; | |||||
| } | |||||
| mat-tree { | |||||
| background-color: transparent; | |||||
| } | |||||
| mat-label { | |||||
| color: white; | |||||
| } | |||||
| .new-node-input { | |||||
| padding-left: 2vw; | |||||
| } |
| import { FlatTreeControl } from '@angular/cdk/tree'; | |||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit } from '@angular/core'; | ||||
| import { FormControl } from '@angular/forms'; | |||||
| import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; | |||||
| import { Rule } from '../../../../api/model/rule'; | |||||
| import { DynamicDatabase } from './class/dynamic-database'; | |||||
| import { RuleFlatNode } from './class/rule-flat-node'; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-create-rule', | selector: 'app-create-rule', | ||||
| }) | }) | ||||
| export class CreateRuleComponent implements OnInit { | export class CreateRuleComponent implements OnInit { | ||||
| constructor() { } | |||||
| flatNodeMap = new Map<RuleFlatNode, Rule>(); | |||||
| nestedNodeMap = new Map<Rule, RuleFlatNode>(); | |||||
| text = new FormControl(''); | |||||
| private _transformer = (rule: Rule, level: number) => { | |||||
| const existingNode = this.nestedNodeMap.get(rule); | |||||
| const flatNode = existingNode && existingNode.item.id === rule.id | |||||
| ? existingNode | |||||
| : new RuleFlatNode(rule, level, !!rule.subRule?.length, false, level === 0) | |||||
| this.flatNodeMap.set(flatNode, rule); | |||||
| // dirty hack to expand node with new child node | |||||
| const lastChildNode = !!rule.subRule ? rule.subRule[rule.subRule.length - 1] : undefined; | |||||
| if (typeof lastChildNode !== 'undefined') { | |||||
| if (flatNode.expandable) { | |||||
| this.treeControl.expand(flatNode); | |||||
| } | |||||
| } | |||||
| return flatNode; | |||||
| } | |||||
| treeControl: FlatTreeControl<RuleFlatNode>; | |||||
| treeFlattener: MatTreeFlattener<Rule, RuleFlatNode>; | |||||
| dataSource: MatTreeFlatDataSource<Rule, RuleFlatNode>; | |||||
| _database: DynamicDatabase; | |||||
| constructor(database: DynamicDatabase) { | |||||
| this._database = database; | |||||
| this.treeControl = new FlatTreeControl<RuleFlatNode>(this.getLevel, this.isExpandable); | |||||
| this.treeFlattener = new MatTreeFlattener<Rule, RuleFlatNode, RuleFlatNode>( | |||||
| this._transformer, this.getLevel, | |||||
| this.isExpandable, this.getChildren | |||||
| ); | |||||
| this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); | |||||
| this._database.dataChange.subscribe(data => { | |||||
| this.dataSource.data = data; | |||||
| }); | |||||
| this._database.childAdded.subscribe(parent => this.treeControl.expand(this.nestedNodeMap.get(parent))) | |||||
| } | |||||
| ngOnInit(): void { | ngOnInit(): void { | ||||
| } | } | ||||
| getLevel = (node: RuleFlatNode) => node.level; | |||||
| isExpandable = (node: RuleFlatNode) => node.expandable; | |||||
| getChildren = (node: Rule): Rule[] => node.subRule; | |||||
| hasChild = (_: number, node: RuleFlatNode) => node.expandable; | |||||
| hasNoContent = (_: number, _nodeData: RuleFlatNode) => _nodeData.item.text === ''; | |||||
| hasContent = (_nodeData: RuleFlatNode) => _nodeData.item.text !== ""; | |||||
| addEmptyRule(parentRuleNode?: RuleFlatNode) { | |||||
| const parentRule = this.flatNodeMap.get(parentRuleNode); | |||||
| if (typeof parentRule === 'undefined') { | |||||
| this._database.insertEmptyParent(); | |||||
| return; | |||||
| } | |||||
| this._database.insertEmptyChild(parentRule); | |||||
| } | |||||
| getParentNode(node: RuleFlatNode): RuleFlatNode | null { | |||||
| const currentLevel = this.getLevel(node); | |||||
| if (currentLevel < 1) { | |||||
| return null; | |||||
| } | |||||
| const startIndex = this.treeControl.dataNodes.indexOf(node) - 1; | |||||
| for (let i = startIndex; i >= 0; i--) { | |||||
| const currentNode = this.treeControl.dataNodes[i]; | |||||
| if (this.getLevel(currentNode) < currentLevel) { | |||||
| return currentNode; | |||||
| } | |||||
| } | |||||
| return null; | |||||
| } | |||||
| async addNode(newNode: RuleFlatNode, text: string) { | |||||
| const newRule = this.flatNodeMap.get(newNode); | |||||
| newRule.text = text; | |||||
| if (!!newRule.parentRule) { | |||||
| const parentRule = this._database.getParentById(newRule.parentRule.id) | |||||
| this._database.insertChild(parentRule, newRule) | |||||
| } | |||||
| this._database.insertParent(newRule) | |||||
| } | |||||
| } | } |
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | import { RouterModule, Routes } from '@angular/router'; | ||||
| import { AuthGuard } from './auth/guard/auth.guard'; | |||||
| import { CreateRuleComponent } from './admin/create-rule/create-rule.component'; | |||||
| import { LoginComponent } from './auth/login/login.component'; | |||||
| import { GridComponent } from './grid/grid.component'; | |||||
| import { RulesComponent } from './rules/rules/rules.component'; | import { RulesComponent } from './rules/rules/rules.component'; | ||||
| const routes: Routes = [ | const routes: Routes = [ | ||||
| { | { | ||||
| path: '', | path: '', | ||||
| component: RulesComponent | component: RulesComponent | ||||
| }, | |||||
| { | |||||
| path: 'login', | |||||
| component: LoginComponent | |||||
| }, | |||||
| { | |||||
| path: 'admin', | |||||
| canActivate: [AuthGuard], | |||||
| children: [ | |||||
| { | |||||
| path: 'create', | |||||
| component: CreateRuleComponent | |||||
| } | |||||
| ] | |||||
| } | } | ||||
| ]; | ]; | ||||
| @NgModule({ | @NgModule({ | ||||
| imports: [RouterModule.forRoot(routes)], | |||||
| imports: [RouterModule.forRoot(routes, { enableTracing: true })], | |||||
| exports: [RouterModule] | exports: [RouterModule] | ||||
| }) | }) | ||||
| export class AppRoutingModule { } | export class AppRoutingModule { } |
| import { MatSidenavModule } from '@angular/material/sidenav'; | import { MatSidenavModule } from '@angular/material/sidenav'; | ||||
| import { MatListModule } from '@angular/material/list'; | import { MatListModule } from '@angular/material/list'; | ||||
| import { RuleListComponent } from './rules/rule-list/rule-list.component'; | import { RuleListComponent } from './rules/rule-list/rule-list.component'; | ||||
| import { AdminModule } from './admin/admin.module'; | |||||
| import { AuthModule } from './auth/auth.module'; | |||||
| import { HTTP_INTERCEPTORS } from '@angular/common/http'; | |||||
| import { AuthInterceptor } from './auth/interceptor/auth.interceptor'; | |||||
| import { ErrorInterceptor } from './auth/interceptor/error.interceptor'; | |||||
| @NgModule({ | @NgModule({ | ||||
| declarations: [ | declarations: [ | ||||
| DashboardComponent, | DashboardComponent, | ||||
| FooterModule, | FooterModule, | ||||
| MatToolbarModule, | MatToolbarModule, | ||||
| MatSidenavModule, | MatSidenavModule, | ||||
| MatListModule | |||||
| MatListModule, | |||||
| AdminModule, | |||||
| AuthModule, | |||||
| ], | |||||
| providers: [ | |||||
| { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, | |||||
| { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true } | |||||
| ], | ], | ||||
| providers: [], | |||||
| bootstrap: [MainComponent] | bootstrap: [MainComponent] | ||||
| }) | }) | ||||
| export class AppModule { } | export class AppModule { } |
| import { NgModule } from '@angular/core'; | |||||
| import { CommonModule } from '@angular/common'; | |||||
| import { AuthGuard } from './guard/auth.guard'; | |||||
| import { AuthService } from './service/auth.service'; | |||||
| import { HttpClientModule } from '@angular/common/http'; | |||||
| import { LoginComponent } from './login/login.component'; | |||||
| import { MatCardModule } from '@angular/material/card'; | |||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | |||||
| import { MatIconModule } from '@angular/material/icon'; | |||||
| import { MatButtonModule } from '@angular/material/button'; | |||||
| import {MatFormFieldModule} from '@angular/material/form-field'; | |||||
| import {MatInputModule} from '@angular/material/input'; | |||||
| @NgModule({ | |||||
| declarations: [ | |||||
| LoginComponent | |||||
| ], | |||||
| imports: [ | |||||
| CommonModule, | |||||
| MatCardModule, | |||||
| MatButtonModule, | |||||
| HttpClientModule, | |||||
| MatFormFieldModule, | |||||
| MatInputModule, | |||||
| MatIconModule, | |||||
| FormsModule, | |||||
| ReactiveFormsModule, | |||||
| ], | |||||
| providers: [ | |||||
| AuthGuard, | |||||
| AuthService, | |||||
| ], | |||||
| }) | |||||
| export class AuthModule { } |
| import { TestBed } from '@angular/core/testing'; | |||||
| import { AuthGuard } from './auth.guard'; | |||||
| describe('AuthGuard', () => { | |||||
| let guard: AuthGuard; | |||||
| beforeEach(() => { | |||||
| TestBed.configureTestingModule({}); | |||||
| guard = TestBed.inject(AuthGuard); | |||||
| }); | |||||
| it('should be created', () => { | |||||
| expect(guard).toBeTruthy(); | |||||
| }); | |||||
| }); |
| import { Injectable } from '@angular/core'; | |||||
| import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; | |||||
| import { Observable } from 'rxjs'; | |||||
| import { AuthService } from '../service/auth.service'; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class AuthGuard implements CanActivate { | |||||
| constructor( | |||||
| private router: Router, | |||||
| private authenticationService: AuthService | |||||
| ) { } | |||||
| canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | |||||
| const user = this.authenticationService.user; | |||||
| if (user) { | |||||
| // logged in so return true | |||||
| return true; | |||||
| } | |||||
| // not logged in so redirect to login page with the return url | |||||
| this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); | |||||
| return false; | |||||
| } | |||||
| } |
| import { TestBed } from '@angular/core/testing'; | |||||
| import { AuthInterceptor } from './auth.interceptor'; | |||||
| describe('AuthInterceptor', () => { | |||||
| beforeEach(() => TestBed.configureTestingModule({ | |||||
| providers: [ | |||||
| AuthInterceptor | |||||
| ] | |||||
| })); | |||||
| it('should be created', () => { | |||||
| const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor); | |||||
| expect(interceptor).toBeTruthy(); | |||||
| }); | |||||
| }); |
| import { Injectable } from '@angular/core'; | |||||
| import { | |||||
| HttpRequest, | |||||
| HttpHandler, | |||||
| HttpEvent, | |||||
| HttpInterceptor | |||||
| } from '@angular/common/http'; | |||||
| import { Observable } from 'rxjs'; | |||||
| import { AuthService } from '../service/auth.service'; | |||||
| import { environment } from '../../../environments/environment'; | |||||
| @Injectable() | |||||
| export class AuthInterceptor implements HttpInterceptor { | |||||
| constructor(private authenticationService: AuthService) { } | |||||
| intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||||
| // add header with basic auth credentials if user is logged in and request is to the api url | |||||
| const user = this.authenticationService.user; | |||||
| const isLoggedIn = user && user.username && user.password; | |||||
| const isAdminUrl = request.url.startsWith(environment.apiUrl); | |||||
| if (isLoggedIn && isAdminUrl) { | |||||
| request = request.clone({ | |||||
| setHeaders: { | |||||
| Authorization: this.authenticationService.getUserToken() | |||||
| } | |||||
| }); | |||||
| } | |||||
| return next.handle(request); | |||||
| } | |||||
| } |
| import { TestBed } from '@angular/core/testing'; | |||||
| import { ErrorInterceptor } from './error.interceptor'; | |||||
| describe('ErrorInterceptor', () => { | |||||
| beforeEach(() => TestBed.configureTestingModule({ | |||||
| providers: [ | |||||
| ErrorInterceptor | |||||
| ] | |||||
| })); | |||||
| it('should be created', () => { | |||||
| const interceptor: ErrorInterceptor = TestBed.inject(ErrorInterceptor); | |||||
| expect(interceptor).toBeTruthy(); | |||||
| }); | |||||
| }); |
| import { Injectable } from '@angular/core'; | |||||
| import { | |||||
| HttpRequest, | |||||
| HttpHandler, | |||||
| HttpEvent, | |||||
| HttpInterceptor | |||||
| } from '@angular/common/http'; | |||||
| import { Observable } from 'rxjs'; | |||||
| import { AuthService } from '../service/auth.service'; | |||||
| import { catchError } from 'rxjs/operators'; | |||||
| import { throwError } from 'rxjs'; | |||||
| @Injectable() | |||||
| export class ErrorInterceptor implements HttpInterceptor { | |||||
| constructor(private authenticationService: AuthService) { } | |||||
| intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||||
| return next.handle(request).pipe(catchError(err => { | |||||
| if (err.status === 401) { | |||||
| // auto logout if 401 response returned from api | |||||
| this.authenticationService.logout(); | |||||
| } | |||||
| const error = err.error.message || err.statusText; | |||||
| return throwError(error); | |||||
| })) | |||||
| } | |||||
| } |
| <mat-card class="wood-card"> | |||||
| <mat-card-title> Login </mat-card-title> | |||||
| <div> | |||||
| <p> | |||||
| <mat-form-field appearance="standard"> | |||||
| <mat-label>Username</mat-label> | |||||
| <input [formControl]="username" matInput type="text" /> | |||||
| </mat-form-field> | |||||
| </p> | |||||
| <p> | |||||
| <mat-form-field appearance="standard"> | |||||
| <mat-label>Password</mat-label> | |||||
| <input [formControl]="password" matInput type="password" /> | |||||
| </mat-form-field> | |||||
| </p> | |||||
| <p> | |||||
| <button (click)="onSubmit()" mat-button>Bestätigen</button> | |||||
| </p> | |||||
| </div> | |||||
| </mat-card> |
| mat-card { | |||||
| color: white; | |||||
| margin: 10vw; | |||||
| padding-left: 10vw; | |||||
| padding-right: 10vw; | |||||
| padding-top: 10vh; | |||||
| padding-bottom: 10vh; | |||||
| } | |||||
| mat-label { | |||||
| color: white; | |||||
| } |
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { LoginComponent } from './login.component'; | |||||
| describe('LoginComponent', () => { | |||||
| let component: LoginComponent; | |||||
| let fixture: ComponentFixture<LoginComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [ LoginComponent ] | |||||
| }) | |||||
| .compileComponents(); | |||||
| }); | |||||
| beforeEach(() => { | |||||
| fixture = TestBed.createComponent(LoginComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); |
| import { Component, OnInit } from '@angular/core'; | |||||
| import { FormControl } from '@angular/forms'; | |||||
| import { ActivatedRoute, Router } from '@angular/router'; | |||||
| import { User } from '../../class/user'; | |||||
| import { AuthService } from '../service/auth.service'; | |||||
| @Component({ | |||||
| selector: 'app-login', | |||||
| templateUrl: './login.component.html', | |||||
| styleUrls: ['./login.component.scss'] | |||||
| }) | |||||
| export class LoginComponent implements OnInit { | |||||
| username = new FormControl(''); | |||||
| password = new FormControl(''); | |||||
| constructor(protected authService: AuthService, private route: ActivatedRoute, protected router: Router) { } | |||||
| onSubmit() { | |||||
| let user = new User(); | |||||
| user.username = this.username.value; | |||||
| user.password = this.password.value; | |||||
| this.authService.user = user; | |||||
| const redirectUrl = this.route.queryParams | |||||
| .subscribe(value => { | |||||
| if (value?.returnUrl) { | |||||
| this.router.navigate([value.returnUrl]); | |||||
| return; | |||||
| } | |||||
| this.router.navigate(['']); | |||||
| }) | |||||
| } | |||||
| ngOnInit(): void { | |||||
| } | |||||
| } |
| import { TestBed } from '@angular/core/testing'; | |||||
| import { AuthService } from './auth.service'; | |||||
| describe('AuthService', () => { | |||||
| let service: AuthService; | |||||
| beforeEach(() => { | |||||
| TestBed.configureTestingModule({}); | |||||
| service = TestBed.inject(AuthService); | |||||
| }); | |||||
| it('should be created', () => { | |||||
| expect(service).toBeTruthy(); | |||||
| }); | |||||
| }); |
| import { Injectable } from '@angular/core'; | |||||
| import { User } from '../../class/user'; | |||||
| import {encode, decode} from 'base-64'; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class AuthService { | |||||
| set user (val: User) { | |||||
| localStorage.setItem("user", encode(`${val.username}:${val.password}`)); | |||||
| } | |||||
| get user () { | |||||
| if (localStorage.getItem("user")) { | |||||
| const userValues = decode(localStorage.getItem("user")).split(':'); | |||||
| const user: User = {username: userValues[0], password: userValues[1]} | |||||
| return user; | |||||
| } | |||||
| return null; | |||||
| } | |||||
| getUserToken () { | |||||
| if (localStorage.getItem("user")) { | |||||
| return `Basic ${localStorage.getItem("user")}`; | |||||
| } | |||||
| return '' | |||||
| } | |||||
| logout() { | |||||
| if (localStorage.getItem("user")) { | |||||
| localStorage.removeItem("user"); | |||||
| } | |||||
| } | |||||
| constructor() { } | |||||
| } |
| import { User } from './user'; | |||||
| describe('User', () => { | |||||
| it('should create an instance', () => { | |||||
| expect(new User()).toBeTruthy(); | |||||
| }); | |||||
| }); |
| export class User { | |||||
| username: string; | |||||
| password: string; | |||||
| } |
| > | > | ||||
| </mat-toolbar> | </mat-toolbar> | ||||
| <main class="content"> | <main class="content"> | ||||
| <app-grid></app-grid> | |||||
| <router-outlet></router-outlet> | |||||
| </main> | </main> | ||||
| </mat-sidenav-content> | </mat-sidenav-content> | ||||
| </mat-sidenav-container> | </mat-sidenav-container> |
| div { | |||||
| color: white; | |||||
| } |
| <mat-card class="dashboard-card rule-card"> | |||||
| <mat-card class="dashboard-card wood-card"> | |||||
| <mat-card-header> | <mat-card-header> | ||||
| <mat-card-title class="title mat-title"> | <mat-card-title class="title mat-title"> | ||||
| <mat-icon mat-list-icon class="logo">gavel</mat-icon> Regeln | <mat-icon mat-list-icon class="logo">gavel</mat-icon> Regeln |
| :host ::ng-deep .mat-list-base .mat-list-item .mat-list-item-content { | |||||
| justify-content: left; | |||||
| text-align: left; | |||||
| } | |||||
| div { | |||||
| color: white; | |||||
| } | |||||
| .rule-card { | |||||
| background-image: url('/assets/wood.jpeg'); | |||||
| background-repeat: no-repeat; | |||||
| background-size: cover; | |||||
| mat-card { | |||||
| margin: 15px; | |||||
| width: 90%; | |||||
| color: white; | color: white; | ||||
| position: absolute; | |||||
| top: 15px; | |||||
| left: 15px; | |||||
| right: 15px; | |||||
| bottom: 15px; | |||||
| } | } |
| export const environment = { | export const environment = { | ||||
| production: true | |||||
| production: true, | |||||
| apiUrl: 'api.hoppe.ziermach.de', | |||||
| }; | }; |
| // The list of file replacements can be found in `angular.json`. | // The list of file replacements can be found in `angular.json`. | ||||
| export const environment = { | export const environment = { | ||||
| production: false | |||||
| production: false, | |||||
| apiUrl: 'http://localhost:3000' | |||||
| }; | }; | ||||
| /* | /* |
| /* You can add global styles to this file, and also import other style files */ | /* You can add global styles to this file, and also import other style files */ | ||||
| @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; | |||||
| html, | html, | ||||
| body { | body { | ||||
| height: 100vh !important; | height: 100vh !important; | ||||
| } | } | ||||
| .logo { | .logo { | ||||
| padding-right: 1vw !important; | padding-right: 1vw !important; | ||||
| } | } | ||||
| display: flex !important; | display: flex !important; | ||||
| align-items: center !important; | align-items: center !important; | ||||
| } | } | ||||
| :host ::ng-deep .mat-list-base .mat-list-item .mat-list-item-content { | |||||
| justify-content: left; | |||||
| text-align: left; | |||||
| } | |||||
| .wood-card { | |||||
| background-image: url('/assets/wood.jpeg'); | |||||
| background-repeat: no-repeat; | |||||
| background-size: cover; | |||||
| color: white; | |||||
| position: absolute; | |||||
| top: 15px; | |||||
| left: 15px; | |||||
| right: 15px; | |||||
| bottom: 15px; | |||||
| } |