Als Nutzer möchte ich Datenänderungen nach verfolgen können #246 #248
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|     "name": "kdb-web", | ||||
|     "version": "1.0.dev247", | ||||
|     "version": "1.0.dev246", | ||||
|     "scripts": { | ||||
|         "ng": "ng", | ||||
|         "update-version": "ts-node-esm update-version.ts", | ||||
| @@ -50,4 +50,4 @@ | ||||
|         "tslib": "^2.4.1", | ||||
|         "typescript": "~4.9.5" | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -2,3 +2,16 @@ export interface Data { | ||||
|   createdAt?: string; | ||||
|   modifiedAt?: string; | ||||
| } | ||||
|  | ||||
| export interface DataWithHistory { | ||||
|   createdAt?: string; | ||||
|   modifiedAt?: string; | ||||
|   history?: History[]; | ||||
| } | ||||
|  | ||||
| export interface History { | ||||
|   deleted?: boolean; | ||||
|   dateFrom?: string; | ||||
|   dateTo?: string; | ||||
|   [x: string | number | symbol]: unknown; | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import { Data } from "./data.model"; | ||||
| import { DataWithHistory } from "./data.model"; | ||||
| import { Level, LevelFilter } from "./level.model"; | ||||
| import { Server, ServerFilter } from "./server.model"; | ||||
| import { UserJoinedServer } from "./user_joined_server.model"; | ||||
| import { UserJoinedVoiceChannel } from "./user_joined_voice_channel.model"; | ||||
| import { UserJoinedGameServer } from "./user_joined_game_server.model"; | ||||
|  | ||||
| export interface User extends Data { | ||||
| export interface User extends DataWithHistory { | ||||
|   id?: number; | ||||
|   discordId?: number; | ||||
|   name?: string; | ||||
| @@ -23,6 +23,17 @@ export interface User extends Data { | ||||
|  | ||||
|   userJoinedGameServerCount?: number; | ||||
|   userJoinedGameServers?: UserJoinedGameServer[]; | ||||
|  | ||||
|   // history?: UserHistory[]; | ||||
| } | ||||
|  | ||||
| export interface UserHistory extends History { | ||||
|   id?: number; | ||||
|   discordId?: number; | ||||
|   xp?: number; | ||||
|   level?: number; | ||||
|   server?: number; | ||||
|   leftServer?: boolean; | ||||
| } | ||||
|  | ||||
| export interface UserFilter { | ||||
|   | ||||
| @@ -115,6 +115,17 @@ export class Queries { | ||||
|  | ||||
|           createdAt | ||||
|           modifiedAt | ||||
|  | ||||
|           history { | ||||
|             id | ||||
|             discordId | ||||
|             xp | ||||
|             server | ||||
|             leftServer | ||||
|             deleted | ||||
|             dateFrom | ||||
|             dateTo | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,21 @@ | ||||
| <button pButton class="btn icon-btn" icon="pi pi-history" (click)="openHistory()"></button> | ||||
|  | ||||
| <p-sidebar styleClass="history p-sidebar-md" [(visible)]="showSidebar" position="right" [baseZIndex]="10000"> | ||||
|   <h1>{{translationKey | translate}} {{'common.history.header' | translate}}</h1> | ||||
|  | ||||
|   <div class="entry-list"> | ||||
|     <div class="entry" *ngFor="let entry of history"> | ||||
|       <div class="attribute" *ngFor="let item of entry | keyvalue"> | ||||
|         <div class="key"> | ||||
|           {{getAttributeTranslationKey(item.key) | translate}} | ||||
|         </div> | ||||
|         <div class="seperator"> | ||||
|           -> | ||||
|         </div> | ||||
|         <div class="value"> | ||||
|           {{item.value}} | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </p-sidebar> | ||||
| @@ -0,0 +1,23 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { HistoryBtnComponent } from './history-btn.component'; | ||||
|  | ||||
| describe('HistoryBtnComponent', () => { | ||||
|   let component: HistoryBtnComponent; | ||||
|   let fixture: ComponentFixture<HistoryBtnComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ HistoryBtnComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|  | ||||
|     fixture = TestBed.createComponent(HistoryBtnComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| 
					
					edraft marked this conversation as resolved
					
				 | ||||
| @@ -0,0 +1,31 @@ | ||||
| import { Component, Input, OnInit } from "@angular/core"; | ||||
| import { History } from "../../../../models/data/data.model"; | ||||
|  | ||||
| @Component({ | ||||
|   selector: "app-history-btn", | ||||
|   templateUrl: "./history-btn.component.html", | ||||
|   styleUrls: ["./history-btn.component.scss"] | ||||
| }) | ||||
| export class HistoryBtnComponent implements OnInit { | ||||
|  | ||||
|   @Input() history: History[] = []; | ||||
|   @Input() translationKey: string = ""; | ||||
|  | ||||
|   showSidebar = false; | ||||
| 
					
					edraft marked this conversation as resolved
					
				 
				
					
						Jonas
						commented  Darf showSidebar keinen Typen bekommen? :( public showSidebar: boolean = false; Je nachdem was showSidebar tut, wäre hier vielleicht ein Store als "Single-Source-Of-Truth" sinnvoll, damit wir hier ein Observable haben. Darf showSidebar keinen Typen bekommen? :(
public showSidebar: boolean = false;
Je nachdem was showSidebar tut, wäre hier vielleicht ein Store als "Single-Source-Of-Truth" sinnvoll, damit wir hier ein Observable haben.
So kann sich egal wo dieser boolische Wert anpassen und DOM-Elemente anschließened gerendert werden oder eben nicht.
Ist aber wie gesagt abhängig vom Use-Case. 
				
					
						edraft
						commented  Generell hast du recht. Wäre für das was die Komponente macht jedoch OP. Generell hast du recht. Wäre für das was die Komponente macht jedoch OP. | ||||
|  | ||||
|   constructor() { | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   openHistory(): void { | ||||
|     console.log("history", this.history); | ||||
|     this.showSidebar = true; | ||||
|   } | ||||
|  | ||||
|   getAttributeTranslationKey(key: string) { | ||||
|     return `common.history.${key}`; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1 @@ | ||||
| <p>history works!</p> | ||||
| @@ -0,0 +1,23 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { HistoryComponent } from './history.component'; | ||||
|  | ||||
| describe('HistoryComponent', () => { | ||||
|   let component: HistoryComponent; | ||||
|   let fixture: ComponentFixture<HistoryComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ HistoryComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|  | ||||
|     fixture = TestBed.createComponent(HistoryComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,10 @@ | ||||
| import { Component } from '@angular/core'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-history', | ||||
|   templateUrl: './history.component.html', | ||||
|   styleUrls: ['./history.component.scss'] | ||||
| }) | ||||
| export class HistoryComponent { | ||||
|  | ||||
| } | ||||
| @@ -1,28 +1,30 @@ | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { HttpClientModule } from '@angular/common/http'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { ButtonModule } from 'primeng/button'; | ||||
| import { CheckboxModule } from 'primeng/checkbox'; | ||||
| import { ConfirmDialogModule } from 'primeng/confirmdialog'; | ||||
| import { DialogModule } from 'primeng/dialog'; | ||||
| import { DropdownModule } from 'primeng/dropdown'; | ||||
| import { DynamicDialogModule } from 'primeng/dynamicdialog'; | ||||
| import { InputTextModule } from 'primeng/inputtext'; | ||||
| import { MenuModule } from 'primeng/menu'; | ||||
| import { PasswordModule } from 'primeng/password'; | ||||
| import { ProgressSpinnerModule } from 'primeng/progressspinner'; | ||||
| import { TableModule } from 'primeng/table'; | ||||
| import { ToastModule } from 'primeng/toast'; | ||||
| import { AuthRolePipe } from './pipes/auth-role.pipe'; | ||||
| import { IpAddressPipe } from './pipes/ip-address.pipe'; | ||||
| import { BoolPipe } from './pipes/bool.pipe'; | ||||
| import { PanelMenuModule } from 'primeng/panelmenu'; | ||||
| import { CommonModule } from "@angular/common"; | ||||
| import { HttpClientModule } from "@angular/common/http"; | ||||
| import { NgModule } from "@angular/core"; | ||||
| import { FormsModule, ReactiveFormsModule } from "@angular/forms"; | ||||
| import { TranslateModule } from "@ngx-translate/core"; | ||||
| import { ButtonModule } from "primeng/button"; | ||||
| import { CheckboxModule } from "primeng/checkbox"; | ||||
| import { ConfirmDialogModule } from "primeng/confirmdialog"; | ||||
| import { DialogModule } from "primeng/dialog"; | ||||
| import { DropdownModule } from "primeng/dropdown"; | ||||
| import { DynamicDialogModule } from "primeng/dynamicdialog"; | ||||
| import { InputTextModule } from "primeng/inputtext"; | ||||
| import { MenuModule } from "primeng/menu"; | ||||
| import { PasswordModule } from "primeng/password"; | ||||
| import { ProgressSpinnerModule } from "primeng/progressspinner"; | ||||
| import { TableModule } from "primeng/table"; | ||||
| import { ToastModule } from "primeng/toast"; | ||||
| import { AuthRolePipe } from "./pipes/auth-role.pipe"; | ||||
| import { IpAddressPipe } from "./pipes/ip-address.pipe"; | ||||
| import { BoolPipe } from "./pipes/bool.pipe"; | ||||
| import { PanelMenuModule } from "primeng/panelmenu"; | ||||
| import { PanelModule } from "primeng/panel"; | ||||
| import { InputNumberModule } from 'primeng/inputnumber'; | ||||
| import { InputNumberModule } from "primeng/inputnumber"; | ||||
| import { ImageModule } from "primeng/image"; | ||||
|  | ||||
| import { SidebarModule } from "primeng/sidebar"; | ||||
| import { HistoryComponent } from "./components/history/history.component"; | ||||
| import { HistoryBtnComponent } from './components/history-btn/history-btn.component'; | ||||
|  | ||||
|  | ||||
| @NgModule({ | ||||
| @@ -30,6 +32,8 @@ import { ImageModule } from "primeng/image"; | ||||
|     AuthRolePipe, | ||||
|     IpAddressPipe, | ||||
|     BoolPipe, | ||||
|     HistoryComponent, | ||||
|     HistoryBtnComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
| @@ -52,7 +56,8 @@ import { ImageModule } from "primeng/image"; | ||||
|     PanelMenuModule, | ||||
|     PanelModule, | ||||
|     InputNumberModule, | ||||
|     ImageModule | ||||
|     ImageModule, | ||||
|     SidebarModule, | ||||
|   ], | ||||
|   exports: [ | ||||
|     ButtonModule, | ||||
| @@ -77,7 +82,9 @@ import { ImageModule } from "primeng/image"; | ||||
|     IpAddressPipe, | ||||
|     BoolPipe, | ||||
|     InputNumberModule, | ||||
|     ImageModule | ||||
|     ImageModule, | ||||
|     SidebarModule, | ||||
|     HistoryBtnComponent | ||||
|   ] | ||||
| }) | ||||
| export class SharedModule { } | ||||
|   | ||||
| @@ -224,6 +224,7 @@ | ||||
|           </td> | ||||
|           <td> | ||||
|             <div class="btn-wrapper"> | ||||
|               <app-history-btn [history]="member.history" translationKey="view.server.members.header"></app-history-btn> | ||||
|               <button *ngIf="!editing" pButton pInitEditableRow class="btn icon-btn" icon="pi pi-pencil" | ||||
|                       (click)="onRowEditInit(dt, member, ri)"></button> | ||||
|               <button *ngIf="!editing" pButton pInitEditableRow class="btn icon-btn" icon="pi pi-user" | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|     "WebVersion": { | ||||
|         "Major": "1", | ||||
|         "Minor": "0", | ||||
|         "Micro": "dev247" | ||||
|         "Micro": "dev246" | ||||
|     }, | ||||
|     "Themes": [ | ||||
|         { | ||||
| @@ -23,4 +23,4 @@ | ||||
|             "Name": "sh-edraft-dark-theme" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -134,7 +134,18 @@ | ||||
|     "modified_at": "Bearbeitet am", | ||||
|     "no_entries_found": "Keine Einträge gefunden", | ||||
|     "of": "von", | ||||
|     "reset_filters": "Filter zurücksetzen" | ||||
|     "reset_filters": "Filter zurücksetzen", | ||||
|     "history": { | ||||
|       "header": "Historie", | ||||
|       "id": "Id", | ||||
|       "discordId": "Discord Id", | ||||
|       "deleted": "Gelöscht", | ||||
|       "dateFrom": "Von", | ||||
|       "dateTo": "Bis", | ||||
|       "server": "Server", | ||||
|       "leftServer": "Gegangen", | ||||
|       "xp": "XP" | ||||
|     } | ||||
|   }, | ||||
|   "dialog": { | ||||
|     "abort": "Abbrechen", | ||||
|   | ||||
| @@ -5,13 +5,39 @@ | ||||
| @import "./styles/primeng-fixes.scss"; | ||||
| @import "./styles/constants.scss"; | ||||
|  | ||||
|  | ||||
| html, | ||||
| body { | ||||
|   height: 100%; | ||||
|   height: 100vh; | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
|  | ||||
|   font-size: 1rem; | ||||
|  | ||||
|   $scrollbarColor: #272727; | ||||
|   $scrollbarBackgroundColor: #888; | ||||
|  | ||||
|   /* width */ | ||||
|   ::-webkit-scrollbar { | ||||
|     width: 10px; | ||||
|   } | ||||
|  | ||||
|   /* Track */ | ||||
|   ::-webkit-scrollbar-track { | ||||
|     background-color: $scrollbarBackgroundColor; | ||||
|   } | ||||
|  | ||||
|   /* Handle */ | ||||
|   ::-webkit-scrollbar-thumb { | ||||
|     background: $scrollbarColor; | ||||
|     border: 3px solid $scrollbarBackgroundColor; | ||||
|     border-radius: 10px; | ||||
|   } | ||||
|  | ||||
|   /* Handle on hover */ | ||||
|   ::-webkit-scrollbar-thumb:hover { | ||||
|     background: $scrollbarColor; | ||||
|   } | ||||
| } | ||||
|  | ||||
| main { | ||||
| @@ -332,6 +358,43 @@ header { | ||||
|   } | ||||
| } | ||||
|  | ||||
| .history { | ||||
|   width: 300px; | ||||
|  | ||||
|   .entry-list { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|  | ||||
|     gap: 15px; | ||||
|  | ||||
|     .entry { | ||||
|       padding-bottom: 10px; | ||||
|  | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|  | ||||
|       gap: 5px; | ||||
|  | ||||
|       .attribute { | ||||
|         display: flex; | ||||
|         gap: 5px; | ||||
|  | ||||
|         .key { | ||||
|           width: 30%; | ||||
|         } | ||||
|  | ||||
|         .seperator { | ||||
|           width: 10%; | ||||
|         } | ||||
|  | ||||
|         .value { | ||||
|           width: 60%; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .p-dialog-header { | ||||
|   padding: 20px 20px 20px 20px !important; | ||||
| } | ||||
|   | ||||
| @@ -175,6 +175,17 @@ | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .history { | ||||
|     background-color: $primaryBackgroundColor; | ||||
|     color: $primaryTextColor; | ||||
|  | ||||
|     .entry-list { | ||||
|       .entry { | ||||
|         border-bottom: 1px solid $primaryHeaderColor; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .p-dialog-header { | ||||
|     background-color: $secondaryBackgroundColor !important; | ||||
|     color: $primaryTextColor !important; | ||||
|   | ||||
| @@ -175,6 +175,17 @@ | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .history { | ||||
|     background-color: $primaryBackgroundColor; | ||||
|     color: $primaryTextColor; | ||||
|  | ||||
|     .entry-list { | ||||
|       .entry { | ||||
|         border-bottom: 1px solid $primaryHeaderColor; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .p-dialog-header { | ||||
|     background-color: $secondaryBackgroundColor !important; | ||||
|     color: $primaryTextColor !important; | ||||
|   | ||||
| @@ -19,6 +19,7 @@ | ||||
|  | ||||
|   background-color: $primaryBackgroundColor; | ||||
|  | ||||
|  | ||||
|   h1, | ||||
|   h2 { | ||||
|     color: $primaryHeaderColor; | ||||
| @@ -175,6 +176,18 @@ | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .history { | ||||
|     background-color: $primaryBackgroundColor; | ||||
|     color: $primaryTextColor; | ||||
|  | ||||
|     .entry-list { | ||||
|       .entry { | ||||
|         border-bottom: 1px solid $primaryHeaderColor; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
|   .p-dialog-header { | ||||
|     background-color: $primaryBackgroundColor !important; | ||||
|     color: $primaryTextColor !important; | ||||
|   | ||||
| @@ -175,6 +175,17 @@ | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .history { | ||||
|     background-color: $primaryBackgroundColor; | ||||
|     color: $primaryTextColor; | ||||
|  | ||||
|     .entry-list { | ||||
|       .entry { | ||||
|         border-bottom: 1px solid $primaryHeaderColor; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .p-dialog-header { | ||||
|     background-color: $secondaryBackgroundColor !important; | ||||
|     color: $primaryTextColor !important; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	
Tests schreiben üben wir aber nochmal ^^