); | ); | ||||
} | } | ||||
/** | |||||
* @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; | |||||
} |