1.0.0 #253
| @@ -180,12 +180,12 @@ class QueryABC(ObjectType): | ||||
|     # @FilterABC.resolve_filter_annotation | ||||
|     def _resolve_collection(self, collection: List, *_, filter: FilterABC = None, page: Page = None, sort: Sort = None): | ||||
|         if filter is not None: | ||||
|             return filter.filter(collection) | ||||
|             collection = filter.filter(collection) | ||||
|  | ||||
|         if page is not None: | ||||
|             return page.filter(collection) | ||||
|             collection = page.filter(collection) | ||||
|  | ||||
|         if sort is not None: | ||||
|             return sort.filter(collection) | ||||
|             collection = sort.filter(collection) | ||||
|  | ||||
|         return collection | ||||
|   | ||||
| @@ -25,7 +25,7 @@ class LevelFilter(FilterABC): | ||||
|         if "server" in values: | ||||
|             from bot_graphql.filter.server_filter import ServerFilter | ||||
|  | ||||
|             self._server: ServerFilter = self._services.get_service(LevelFilter) | ||||
|             self._server: ServerFilter = self._services.get_service(ServerFilter) | ||||
|             self._server.from_dict(values["server"]) | ||||
|  | ||||
|     def filter(self, query: List[Level]) -> List[Level]: | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from cpl_discord.service import DiscordBotServiceABC | ||||
| from cpl_query.extension import List | ||||
|  | ||||
| from bot_core.abc.client_utils_abc import ClientUtilsABC | ||||
| from bot_data.abc.user_joined_server_repository_abc import UserJoinedServerRepositoryABC | ||||
| from bot_data.model.user import User | ||||
| from bot_graphql.abc.filter_abc import FilterABC | ||||
| from bot_graphql.filter.level_filter import LevelFilter | ||||
| @@ -18,6 +19,7 @@ class UserFilter(FilterABC): | ||||
|         bot: DiscordBotServiceABC, | ||||
|         client_utils: ClientUtilsABC, | ||||
|         levels: LevelService, | ||||
|         user_joined_servers: UserJoinedServerRepositoryABC, | ||||
|     ): | ||||
|         FilterABC.__init__(self) | ||||
|  | ||||
| @@ -25,6 +27,7 @@ class UserFilter(FilterABC): | ||||
|         self._bot = bot | ||||
|         self._client_utils = client_utils | ||||
|         self._levels = levels | ||||
|         self._user_joined_servers = user_joined_servers | ||||
|  | ||||
|         self._id = None | ||||
|         self._discord_id = None | ||||
| @@ -34,6 +37,7 @@ class UserFilter(FilterABC): | ||||
|         self._ontime = None | ||||
|         self._level: Optional[LevelFilter] = None | ||||
|         self._server = None | ||||
|         self._left_server = None | ||||
|  | ||||
|     def from_dict(self, values: dict): | ||||
|         if "id" in values: | ||||
| @@ -64,18 +68,26 @@ class UserFilter(FilterABC): | ||||
|             self._server: ServerFilter = self._services.get_service(ServerFilter) | ||||
|             self._server.from_dict(values["server"]) | ||||
|  | ||||
|         if "leftServer" in values: | ||||
|             self._left_server = values["leftServer"] | ||||
|  | ||||
|     def filter(self, query: List[User]) -> List[User]: | ||||
|         if self._id is not None: | ||||
|             query = query.where(lambda x: x.user_id == self._id) | ||||
|             query = query.where(lambda x: x.user_id == self._id or str(self._id) in str(x.user_id)) | ||||
|  | ||||
|         if self._discord_id is not None: | ||||
|             query = query.where(lambda x: x.discord_id == self._discord_id) | ||||
|             query = query.where( | ||||
|                 lambda x: x.discord_id == self._discord_id or str(self._discord_id) in str(x.discord_id) | ||||
|             ) | ||||
|  | ||||
|         if self._name is not None: | ||||
|             query = query.where( | ||||
|                 lambda x: self._bot.get_user(x.discord_id).name == self._name | ||||
|                 or self._name in self._bot.get_user(x.discord_id).name | ||||
|             ) | ||||
|  | ||||
|             def _get_member(user: User): | ||||
|                 guild = self._bot.get_guild(user.server.discord_server_id) | ||||
|                 member = guild.get_member(user.discord_id) | ||||
|                 return member is not None and (member.name == self._name or self._name in member.name) | ||||
|  | ||||
|             query = query.where(_get_member) | ||||
|  | ||||
|         if self._xp is not None: | ||||
|             query = query.where(lambda x: x.xp == self._xp) | ||||
| @@ -94,4 +106,12 @@ class UserFilter(FilterABC): | ||||
|             servers = self._server.filter(query.select(lambda x: x.server)).select(lambda x: x.server_id) | ||||
|             query = query.where(lambda x: x.server.server_id in servers) | ||||
|  | ||||
|         if self._left_server is not None: | ||||
|  | ||||
|             def _has_user_left_server(user: User): | ||||
|                 active_join = self._user_joined_servers.find_active_user_joined_server_by_user_id(user.user_id) | ||||
|                 return (active_join is None) == self._left_server | ||||
|  | ||||
|             query = query.where(_has_user_left_server) | ||||
|  | ||||
|         return query | ||||
|   | ||||
| @@ -17,6 +17,7 @@ type User implements TableQuery { | ||||
|     userJoinedGameServers(filter: UserJoinedGameServerFilter, page: Page, sort: Sort): [UserJoinedGameServer] | ||||
|  | ||||
|     server: Server | ||||
|     leftServer: Boolean | ||||
|  | ||||
|     createdAt: String | ||||
|     modifiedAt: String | ||||
| @@ -31,6 +32,7 @@ input UserFilter { | ||||
|     ontime: Float | ||||
|     level: LevelFilter | ||||
|     server: ServerFilter | ||||
|     leftServer: Boolean | ||||
| } | ||||
|  | ||||
| type UserMutation { | ||||
| @@ -40,4 +42,5 @@ type UserMutation { | ||||
| input UserInput { | ||||
|     id: ID | ||||
|     xp: Int | ||||
|     levelId: ID | ||||
| } | ||||
| @@ -1,10 +1,12 @@ | ||||
| from cpl_core.database.context import DatabaseContextABC | ||||
| from cpl_discord.service import DiscordBotServiceABC | ||||
|  | ||||
| from bot_data.abc.level_repository_abc import LevelRepositoryABC | ||||
| from bot_data.abc.server_repository_abc import ServerRepositoryABC | ||||
| from bot_data.abc.user_repository_abc import UserRepositoryABC | ||||
| from bot_data.model.user_role_enum import UserRoleEnum | ||||
| from bot_graphql.abc.query_abc import QueryABC | ||||
| from modules.level.service.level_service import LevelService | ||||
| from modules.permission.service.permission_service import PermissionService | ||||
|  | ||||
|  | ||||
| @@ -16,6 +18,8 @@ class UserMutation(QueryABC): | ||||
|         bot: DiscordBotServiceABC, | ||||
|         db: DatabaseContextABC, | ||||
|         permissions: PermissionService, | ||||
|         levels: LevelRepositoryABC, | ||||
|         level_service: LevelService, | ||||
|     ): | ||||
|         QueryABC.__init__(self, "UserMutation") | ||||
|  | ||||
| @@ -24,6 +28,8 @@ class UserMutation(QueryABC): | ||||
|         self._bot = bot | ||||
|         self._db = db | ||||
|         self._permissions = permissions | ||||
|         self._levels = levels | ||||
|         self._level_service = level_service | ||||
|  | ||||
|         self.set_field("updateUser", self.resolve_update_user) | ||||
|  | ||||
| @@ -33,8 +39,13 @@ class UserMutation(QueryABC): | ||||
|  | ||||
|         user.xp = input["xp"] if "xp" in input else user.xp | ||||
|  | ||||
|         if "levelId" in input: | ||||
|             level = self._levels.get_level_by_id(input["levelId"]) | ||||
|             user.xp = level.min_xp | ||||
|  | ||||
|         self._users.update_user(user) | ||||
|         self._db.save_changes() | ||||
|         self._level_service.set_level(user) | ||||
|  | ||||
|         user = self._users.get_user_by_id(input["id"]) | ||||
|         return user | ||||
|   | ||||
| @@ -57,6 +57,7 @@ class UserQuery(DataQueryABC): | ||||
|             UserJoinedGameServerFilter, | ||||
|         ) | ||||
|         self.set_field("server", self.resolve_server) | ||||
|         self.set_field("leftServer", self.resolve_left_server) | ||||
|  | ||||
|     @staticmethod | ||||
|     def resolve_id(user: User, *_): | ||||
| @@ -88,3 +89,6 @@ class UserQuery(DataQueryABC): | ||||
|     @staticmethod | ||||
|     def resolve_server(user: User, *_): | ||||
|         return user.server | ||||
|  | ||||
|     def resolve_left_server(self, user: User, *_): | ||||
|         return self._ujs.find_active_user_joined_server_by_user_id(user.user_id) is None | ||||
|   | ||||
							
								
								
									
										17
									
								
								kdb-web/src/app/models/data/level.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								kdb-web/src/app/models/data/level.model.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { Data } from "./data.model"; | ||||
| import { Server, ServerFilter } from "./server.model"; | ||||
|  | ||||
| export interface Level extends Data { | ||||
|   id?: number; | ||||
|   name?: string; | ||||
|   color?: string; | ||||
|   minXp?: number; | ||||
|   permissions?: string; | ||||
|   server?: Server; | ||||
| } | ||||
|  | ||||
| export interface LevelFilter { | ||||
|   id?: number; | ||||
|   name?: String; | ||||
|   server?: ServerFilter; | ||||
| } | ||||
| @@ -1,8 +1,10 @@ | ||||
| import { Data } from "./data.model"; | ||||
| import { User } from "./user.model"; | ||||
| import { Level } from "./level.model"; | ||||
|  | ||||
| export interface Server extends Data { | ||||
|   id?: number; | ||||
|   discordId?: number; | ||||
|   discordId?: String; | ||||
|   name?: string; | ||||
|   iconURL?: string; | ||||
|   autoRoleCount?: number; | ||||
| @@ -10,7 +12,13 @@ export interface Server extends Data { | ||||
|   clientCount?: number; | ||||
|   clients?: []; | ||||
|   levelCount?: number; | ||||
|   levels?: []; | ||||
|   levels?: Level[]; | ||||
|   userCount?: number; | ||||
|   users?: []; | ||||
|   users?: User[]; | ||||
| } | ||||
|  | ||||
| export interface ServerFilter { | ||||
|   id?: number; | ||||
|   discordId?: String; | ||||
|   name?: String; | ||||
| } | ||||
|   | ||||
							
								
								
									
										36
									
								
								kdb-web/src/app/models/data/user.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								kdb-web/src/app/models/data/user.model.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import { Data } from "./data.model"; | ||||
| import { Level, LevelFilter } from "./level.model"; | ||||
| import { Server, ServerFilter } from "./server.model"; | ||||
|  | ||||
| export interface User extends Data { | ||||
|   id?: number; | ||||
|   discordId?: number; | ||||
|   name?: string; | ||||
|   xp?: number; | ||||
|   minecraftId?: number; | ||||
|   ontime?: number; | ||||
|   level?: Level; | ||||
|   server?: Server; | ||||
|   leftServer?: boolean; | ||||
|  | ||||
|   joinedServerCount?: number; | ||||
|   joinedServers?: []; | ||||
|  | ||||
|   joinedVoiceChannelCount?: number; | ||||
|   joinedVoiceChannels?: []; | ||||
|  | ||||
|   userJoinedGameServerCount?: number; | ||||
|   userJoinedGameServers?: []; | ||||
| } | ||||
|  | ||||
| export interface UserFilter { | ||||
|   id?: number; | ||||
|   discordId?: number; | ||||
|   name?: string; | ||||
|   xp?: number; | ||||
|   minecraftId?: number; | ||||
|   ontime?: number; | ||||
|   level?: LevelFilter; | ||||
|   server?: ServerFilter; | ||||
|   leftServer?: boolean; | ||||
| } | ||||
							
								
								
									
										17
									
								
								kdb-web/src/app/models/graphql/mutations.model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								kdb-web/src/app/models/graphql/mutations.model.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| export class Mutations { | ||||
|   static updateUser = ` | ||||
|     mutation updateUser($id: ID, $xp: Int, $levelId: ID) { | ||||
|       user { | ||||
|         updateUser(input: { id: $id, xp: $xp, levelId: $levelId }) { | ||||
|           id | ||||
|           name | ||||
|           xp | ||||
|           level { | ||||
|             id | ||||
|             name | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   `; | ||||
| } | ||||
| @@ -21,4 +21,61 @@ export class Queries { | ||||
|       } | ||||
|     } | ||||
|   `; | ||||
|  | ||||
|   static levelQuery= ` | ||||
|     query LevelsList($filter: LevelFilter, $page: Page, $sort: Sort) { | ||||
|       levelCount | ||||
|       levels(filter: $filter, page: $page, sort: $sort) { | ||||
|         id | ||||
|         name | ||||
|         color | ||||
|         minXp | ||||
|         permissions | ||||
|         server { | ||||
|           id | ||||
|           name | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   `; | ||||
|  | ||||
|   static usersQuery= ` | ||||
|     query UsersList($filter: UserFilter, $page: Page, $sort: Sort) { | ||||
|       userCount | ||||
|       users(filter: $filter, page: $page, sort: $sort) { | ||||
|         id | ||||
|         discordId | ||||
|         name | ||||
|         xp | ||||
|         ontime | ||||
|         level { | ||||
|           id | ||||
|           name | ||||
|         } | ||||
|         server { | ||||
|           id | ||||
|           name | ||||
|         } | ||||
|         leftServer | ||||
|  | ||||
|         joinedServerCount | ||||
|         joinedServers { | ||||
|           id | ||||
|         } | ||||
|  | ||||
|         joinedVoiceChannelCount | ||||
|         joinedVoiceChannels { | ||||
|           id | ||||
|           channelId | ||||
|           channelName | ||||
|         } | ||||
|  | ||||
|         userJoinedGameServerCount | ||||
|         userJoinedGameServers { | ||||
|           id | ||||
|           gameServer | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,18 @@ | ||||
| import { Server } from "../data/server.model"; | ||||
| import { User } from "../data/user.model"; | ||||
|  | ||||
| export interface Query { | ||||
|   serverCount: number; | ||||
|   servers: Server[]; | ||||
| } | ||||
|  | ||||
| export interface UserListQuery { | ||||
|   userCount: number; | ||||
|   users: User[]; | ||||
| } | ||||
|  | ||||
| export interface LevelListQuery { | ||||
|   levelCount: number; | ||||
|   levels: User[]; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| import { Query } from "./query.model"; | ||||
| import { User } from "../data/user.model"; | ||||
|  | ||||
| export interface QueryResult { | ||||
|   data: any; | ||||
| } | ||||
|  | ||||
| export interface UpdateUserMutationResult { | ||||
|   user: { | ||||
|     updateUser: User | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -32,7 +32,7 @@ | ||||
|           <th pSortableColumn="firstName"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'admin.auth_users.headers.first_name' | translate}}</div> | ||||
|               <p-sortIcon icon="sort" field="firstName" class="table-header-icon"></p-sortIcon> | ||||
|               <p-sortIcon field="firstName" class="table-header-icon"></p-sortIcon> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
| @@ -70,24 +70,11 @@ | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th class="table-header-small-dropdown"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'admin.auth_users.headers.created_at' | translate}}</div> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th class="table-header-small-dropdown"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'admin.auth_users.headers.modified_at' | translate}}</div> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th class="table-header-actions"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'admin.auth_users.headers.actions' | translate}}</div> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th> | ||||
| @@ -108,7 +95,7 @@ | ||||
|           <th></th> | ||||
|           <th> | ||||
|             <form [formGroup]="filterForm"> | ||||
|               <p-dropdown formControlName="authRole" [options]="authRoles"></p-dropdown> | ||||
|               <p-dropdown formControlName="authRole" [options]="authRoles" placeholder="{{'admin.auth_users.headers.auth_role' | translate}}"></p-dropdown> | ||||
|             </form> | ||||
|           </th> | ||||
|           <th></th> | ||||
| @@ -223,7 +210,7 @@ | ||||
|         </tr> | ||||
|       </ng-template> | ||||
|  | ||||
|       <ng-template pTemplate="Invalidmessage"> | ||||
|       <ng-template pTemplate="emptymessage"> | ||||
|         <tr> | ||||
|           <td colspan="7">{{'admin.auth_users.no_entries_found' | translate}}</td> | ||||
|         </tr> | ||||
| @@ -234,3 +221,4 @@ | ||||
|     </p-table> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,192 @@ | ||||
| <h1> | ||||
|   {{'view.server.members.header' | translate}} | ||||
| </h1> | ||||
| <div class="content-wrapper"> | ||||
|   <div class="content"> | ||||
|     <p-table #dt [value]="members" dataKey="id" editMode="row" [rowHover]="true" [rows]="10" | ||||
|              [rowsPerPageOptions]="[10,25,50]" [paginator]="true" [loading]="loading" [totalRecords]="totalRecords" | ||||
|              [lazy]="true" (onLazyLoad)="nextPage($event)"> | ||||
|  | ||||
|       <ng-template pTemplate="caption"> | ||||
|         <div class="table-caption"> | ||||
|           <div class="table-caption-text"> | ||||
|             <ng-container *ngIf="!loading">{{members.length}} {{'view.server.members.of' | translate}} | ||||
|               {{dt.totalRecords}} | ||||
|             </ng-container> | ||||
|             {{'view.server.members.members' | translate}} | ||||
|           </div> | ||||
|  | ||||
|           <div class="table-caption-btn-wrapper btn-wrapper"> | ||||
|             <button pButton label="{{'view.server.members.reset_filters' | translate}}" icon="pi pi-undo" | ||||
|                     class="icon-btn btn" (click)="resetFilters()"> | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </ng-template> | ||||
|  | ||||
|       <ng-template pTemplate="header"> | ||||
|         <tr> | ||||
|           <th pSortableColumn="id"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'view.server.members.headers.id' | translate}}</div> | ||||
|               <p-sortIcon field="id" class="table-header-icon"></p-sortIcon> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th pSortableColumn="discordId"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'view.server.members.headers.discord_id' | translate}}</div> | ||||
|               <p-sortIcon field="discordId" class="table-header-icon"></p-sortIcon> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th pSortableColumn="name"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'view.server.members.headers.name' | translate}}</div> | ||||
|               <p-sortIcon field="name" class="table-header-icon"></p-sortIcon> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th pSortableColumn="xp"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'view.server.members.headers.xp' | translate}}</div> | ||||
|               <p-sortIcon field="xp" class="table-header-icon"></p-sortIcon> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th pSortableColumn="ontime"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'view.server.members.headers.ontime' | translate}}</div> | ||||
|               <p-sortIcon field="ontime" class="table-header-icon"></p-sortIcon> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th class="table-header-small-dropdown" pSortableColumn="level"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'view.server.members.headers.level' | translate}}</div> | ||||
|               <p-sortIcon field="level" class="table-header-icon"></p-sortIcon> | ||||
|             </div> | ||||
|           </th> | ||||
|  | ||||
|           <th class="table-header-actions"> | ||||
|             <div class="table-header-label"> | ||||
|               <div class="table-header-text">{{'view.server.members.headers.actions' | translate}}</div> | ||||
|             </div> | ||||
|           </th> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th> | ||||
|             <form [formGroup]="filterForm"> | ||||
|               <input type="text" pInputText formControlName="id" placeholder="{{'view.server.members.headers.id' | translate}}"> | ||||
|             </form> | ||||
|           </th> | ||||
|           <th> | ||||
|             <form [formGroup]="filterForm"> | ||||
|               <input type="text" pInputText formControlName="discordId" placeholder="{{'view.server.members.headers.discord_id' | translate}}"> | ||||
|             </form> | ||||
|           </th> | ||||
|           <th> | ||||
|             <form [formGroup]="filterForm"> | ||||
|               <input type="text" pInputText formControlName="name" placeholder="{{'view.server.members.headers.name' | translate}}"> | ||||
|             </form> | ||||
|           </th> | ||||
|           <th></th> | ||||
|           <th></th> | ||||
|           <th> | ||||
|             <form [formGroup]="filterForm"> | ||||
|               <p-dropdown formControlName="level" [options]="levels" placeholder="{{'view.server.members.headers.level' | translate}}"></p-dropdown> | ||||
|             </form> | ||||
|           </th> | ||||
|           <th></th> | ||||
|         </tr> | ||||
|       </ng-template> | ||||
|  | ||||
|       <ng-template pTemplate="body" let-member let-editing="editing" let-ri="rowIndex"> | ||||
|         <tr [pEditableRow]="member"> | ||||
|           <td> | ||||
|             <p-cellEditor> | ||||
|               <ng-template pTemplate="input"> | ||||
|                 {{member.id}} | ||||
|               </ng-template> | ||||
|               <ng-template pTemplate="output"> | ||||
|                 {{member.id}} | ||||
|               </ng-template> | ||||
|             </p-cellEditor> | ||||
|           </td> | ||||
|           <td> | ||||
|             <p-cellEditor> | ||||
|               <ng-template pTemplate="input"> | ||||
|                 {{member.discordId}} | ||||
|               </ng-template> | ||||
|               <ng-template pTemplate="output"> | ||||
|                 {{member.discordId}} | ||||
|               </ng-template> | ||||
|             </p-cellEditor> | ||||
|           </td> | ||||
|           <td> | ||||
|             <p-cellEditor> | ||||
|               <ng-template pTemplate="input"> | ||||
|                 {{member.name}} | ||||
|               </ng-template> | ||||
|               <ng-template pTemplate="output"> | ||||
|                 {{member.name}} | ||||
|               </ng-template> | ||||
|             </p-cellEditor> | ||||
|           </td> | ||||
|           <td> | ||||
|             <p-cellEditor> | ||||
|               <ng-template pTemplate="input"> | ||||
|                 <input class="table-edit-input" pInputText type="number" [(ngModel)]="member.xp"> | ||||
|               </ng-template> | ||||
|               <ng-template pTemplate="output"> | ||||
|                 {{member.xp}} | ||||
|               </ng-template> | ||||
|             </p-cellEditor> | ||||
|           </td> | ||||
|           <td> | ||||
|             <p-cellEditor> | ||||
|               <ng-template pTemplate="input"> | ||||
|                 {{member.ontime}} | ||||
|               </ng-template> | ||||
|               <ng-template pTemplate="output"> | ||||
|                 {{member.ontime}} | ||||
|               </ng-template> | ||||
|             </p-cellEditor> | ||||
|           </td> | ||||
|           <td> | ||||
|             <p-cellEditor> | ||||
|               <ng-template pTemplate="input"> | ||||
|                 <p-dropdown [options]="levels" [(ngModel)]="member.level" placeholder="{{'view.server.members.headers.level' | translate}}"></p-dropdown> | ||||
|               </ng-template> | ||||
|               <ng-template pTemplate="output"> | ||||
|                 {{member.level.name}} | ||||
|               </ng-template> | ||||
|             </p-cellEditor> | ||||
|           </td> | ||||
|           <td> | ||||
|             <div class="btn-wrapper"> | ||||
|               <button *ngIf="!editing" pButton pInitEditableRow class="btn icon-btn" icon="pi pi-pencil" | ||||
|                       (click)="onRowEditInit(dt, member, ri)"></button> | ||||
|  | ||||
|               <button *ngIf="editing" pButton pSaveEditableRow class="btn icon-btn" | ||||
|                       icon="pi pi-check-circle" (click)="onRowEditSave(dt, member, ri)"></button> | ||||
|               <button *ngIf="editing" pButton pCancelEditableRow class="btn icon-btn danger-icon-btn" | ||||
|                       icon="pi pi-times-circle" (click)="onRowEditCancel(member, ri)"></button> | ||||
|             </div> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </ng-template> | ||||
|  | ||||
|       <ng-template pTemplate="emptymessage"> | ||||
|         <tr></tr> | ||||
|         <tr> | ||||
|           <td colspan="7">{{'view.server.members.no_entries_found' | translate}}</td> | ||||
|         </tr> | ||||
|         <tr></tr> | ||||
|       </ng-template> | ||||
|  | ||||
|       <ng-template pTemplate="paginatorleft"> | ||||
|       </ng-template> | ||||
|     </p-table> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,23 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { MembersComponent } from './members.component'; | ||||
|  | ||||
| describe('MembersComponent', () => { | ||||
|   let component: MembersComponent; | ||||
|   let fixture: ComponentFixture<MembersComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ MembersComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|  | ||||
|     fixture = TestBed.createComponent(MembersComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										235
									
								
								kdb-web/src/app/modules/view/server/members/members.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								kdb-web/src/app/modules/view/server/members/members.component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,235 @@ | ||||
| import { Component } from "@angular/core"; | ||||
| import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; | ||||
| import { AuthService } from "../../../../services/auth/auth.service"; | ||||
| import { SpinnerService } from "../../../../services/spinner/spinner.service"; | ||||
| import { ToastService } from "../../../../services/toast/toast.service"; | ||||
| import { ConfirmationDialogService } from "../../../../services/confirmation-dialog/confirmation-dialog.service"; | ||||
| import { TranslateService } from "@ngx-translate/core"; | ||||
| import { catchError, debounceTime } from "rxjs/operators"; | ||||
| import { LazyLoadEvent, MenuItem } from "primeng/api"; | ||||
| import { Table } from "primeng/table"; | ||||
| import { User, UserFilter } from "../../../../models/data/user.model"; | ||||
| import { Queries } from "../../../../models/graphql/queries.model"; | ||||
| import { LevelListQuery, UserListQuery } from "../../../../models/graphql/query.model"; | ||||
| import { DataService } from "../../../../services/data/data.service"; | ||||
| import { Page } from "../../../../models/graphql/filter/page.model"; | ||||
| import { Sort } from "../../../../models/graphql/filter/sort.model"; | ||||
| import { SidebarService } from "../../../../services/sidebar/sidebar.service"; | ||||
| import { Mutations } from "../../../../models/graphql/mutations.model"; | ||||
| import { throwError } from "rxjs"; | ||||
| import { UpdateUserMutationResult } from "../../../../models/graphql/result.model"; | ||||
|  | ||||
| @Component({ | ||||
|   selector: "app-members", | ||||
|   templateUrl: "./members.component.html", | ||||
|   styleUrls: ["./members.component.scss"] | ||||
| }) | ||||
| export class MembersComponent { | ||||
|   members!: User[]; | ||||
|   // levelsFilter!: MenuItem[]; | ||||
|   levels!: MenuItem[]; | ||||
|   loading = true; | ||||
|  | ||||
|   clonedUsers: { [s: string]: User; } = {}; | ||||
|   isEditingNew: boolean = false; | ||||
|  | ||||
|   newUserTemplate: User = { | ||||
|     id: 0, | ||||
|     discordId: 0, | ||||
|     name: "", | ||||
|     xp: 0, | ||||
|     ontime: 0, | ||||
|     level: undefined, | ||||
|     server: undefined, | ||||
|  | ||||
|     joinedServerCount: 0, | ||||
|     joinedServers: [], | ||||
|  | ||||
|     joinedVoiceChannelCount: 0, | ||||
|     joinedVoiceChannels: [], | ||||
|  | ||||
|     userJoinedGameServerCount: 0, | ||||
|     userJoinedGameServers: [], | ||||
|  | ||||
|     createdAt: "", | ||||
|     modifiedAt: "" | ||||
|   }; | ||||
|  | ||||
|   filterForm!: FormGroup<{ | ||||
|     id: FormControl<number | null>, | ||||
|     discordId: FormControl<number | null>, | ||||
|     name: FormControl<string | null>, | ||||
|     level: FormControl<number | null> | ||||
|   }>; | ||||
|  | ||||
|   filter: UserFilter = { | ||||
|     leftServer: false | ||||
|   }; | ||||
|   page: Page = { | ||||
|     pageSize: undefined, | ||||
|     pageIndex: undefined | ||||
|   }; | ||||
|   sort: Sort = { | ||||
|     sortColumn: undefined, | ||||
|     sortDirection: undefined | ||||
|   }; | ||||
|  | ||||
|   totalRecords!: number; | ||||
|  | ||||
|   constructor( | ||||
|     private authService: AuthService, | ||||
|     private spinner: SpinnerService, | ||||
|     private toastService: ToastService, | ||||
|     private confirmDialog: ConfirmationDialogService, | ||||
|     private fb: FormBuilder, | ||||
|     private translate: TranslateService, | ||||
|     private data: DataService, | ||||
|     private sidebar: SidebarService | ||||
|   ) { | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.spinner.showSpinner(); | ||||
|     this.data.query<LevelListQuery>(Queries.levelQuery, { | ||||
|         filter: { | ||||
|           server: { id: this.sidebar.server$.value?.id } | ||||
|         } | ||||
|       } | ||||
|     ).subscribe(data => { | ||||
|       this.levels = data.levels.map(level => { | ||||
|         return { label: level.name, value: level }; | ||||
|       }); | ||||
|       this.spinner.hideSpinner(); | ||||
|     }); | ||||
|  | ||||
|     this.setFilterForm(); | ||||
|     this.loadNextPage(); | ||||
|   } | ||||
|  | ||||
|   setFilterForm() { | ||||
|     this.filterForm = this.fb.group({ | ||||
|       id: new FormControl<number | null>(null), | ||||
|       discordId: new FormControl<number | null>(null), | ||||
|       name: [""], | ||||
|       level: new FormControl<number | null>(null) | ||||
|     }); | ||||
|  | ||||
|     this.filterForm.valueChanges.pipe( | ||||
|       debounceTime(600) | ||||
|     ).subscribe(changes => { | ||||
|       if (changes.id) { | ||||
|         this.filter.id = changes.id; | ||||
|       } else { | ||||
|         this.filter.id = undefined; | ||||
|       } | ||||
|  | ||||
|       if (changes.discordId) { | ||||
|         this.filter.discordId = changes.discordId; | ||||
|       } else { | ||||
|         this.filter.discordId = undefined; | ||||
|       } | ||||
|  | ||||
|       if (changes.name) { | ||||
|         this.filter.name = changes.name; | ||||
|       } else { | ||||
|         this.filter.name = undefined; | ||||
|       } | ||||
|  | ||||
|       if (changes.level) { | ||||
|         this.filter.level = { | ||||
|           id: changes.level | ||||
|         }; | ||||
|       } else { | ||||
|         this.filter.level = undefined; | ||||
|       } | ||||
|  | ||||
|       if (this.page.pageSize) | ||||
|         this.page.pageSize = 10; | ||||
|  | ||||
|       if (this.page.pageIndex) | ||||
|         this.page.pageIndex = 0; | ||||
|  | ||||
|       this.loadNextPage(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   loadNextPage() { | ||||
|     this.loading = true; | ||||
|     this.data.query<UserListQuery>(Queries.usersQuery, { | ||||
|         filter: this.filter, page: this.page, sort: this.sort | ||||
|       } | ||||
|     ).subscribe(data => { | ||||
|       this.totalRecords = data.userCount; | ||||
|       this.members = data.users; | ||||
|       this.spinner.hideSpinner(); | ||||
|       this.loading = false; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   nextPage(event: LazyLoadEvent) { | ||||
|     this.page.pageSize = event.rows ?? 0; | ||||
|     if (event.first != null && event.rows != null) | ||||
|       this.page.pageIndex = event.first / event.rows; | ||||
|     this.sort.sortColumn = event.sortField ?? undefined; | ||||
|     this.sort.sortDirection = event.sortOrder === 1 ? "asc" : event.sortOrder === -1 ? "desc" : "asc"; | ||||
|  | ||||
|     this.loadNextPage(); | ||||
|   } | ||||
|  | ||||
|   resetFilters() { | ||||
|     this.filterForm.reset(); | ||||
|   } | ||||
|  | ||||
|   onRowEditInit(table: Table, user: User, index: number) { | ||||
|     this.clonedUsers[index] = { ...user }; | ||||
|   } | ||||
|  | ||||
|   onRowEditSave(table: Table, newUser: User, index: number) { | ||||
|     // const oldUser = this.clonedUsers[index]; | ||||
|     // delete this.clonedUsers[index]; | ||||
|  | ||||
|     // if (JSON.stringify(oldUser) === JSON.stringify(newUser) && !this.isEditingNew) { | ||||
|     //   console.log(1, oldUser, newUser, JSON.stringify(oldUser) === JSON.stringify(newUser), !this.isEditingNew); | ||||
|     //   return; | ||||
|     // } | ||||
|  | ||||
|     if (this.isEditingNew && JSON.stringify(newUser) === JSON.stringify(this.newUserTemplate)) { | ||||
|       this.isEditingNew = false; | ||||
|       this.members.splice(index, 1); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (!newUser.id || !newUser.xp && !newUser.level?.id) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this.spinner.showSpinner(); | ||||
|     this.data.mutation<UpdateUserMutationResult>(Mutations.updateUser, { | ||||
|         id: newUser.id, | ||||
|         xp: newUser.xp, | ||||
|         levelId: newUser.level?.id | ||||
|       } | ||||
|     ).pipe(catchError(err => { | ||||
|       this.spinner.hideSpinner(); | ||||
|       this.toastService.error(this.translate.instant("view.server.members.message.user_change_failed"), this.translate.instant("view.server.members.message.user_change_failed_d", { name: newUser.name })); | ||||
|       return throwError(err); | ||||
|     })).subscribe(_ => { | ||||
|       this.spinner.hideSpinner(); | ||||
|       this.toastService.success(this.translate.instant("view.server.members.message.user_changed"), this.translate.instant("view.server.members.message.user_changed_d", { name: newUser.name })); | ||||
|       this.loadNextPage(); | ||||
|     }); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   onRowEditCancel(user: User, index: number) { | ||||
|     if (this.isEditingNew) { | ||||
|       this.members.splice(index, 1); | ||||
|       delete this.clonedUsers[index]; | ||||
|       this.isEditingNew = false; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this.members[index] = this.clonedUsers[index]; | ||||
|     delete this.clonedUsers[index]; | ||||
|   } | ||||
| } | ||||
| @@ -2,10 +2,12 @@ import { NgModule } from "@angular/core"; | ||||
| import { RouterModule, Routes } from "@angular/router"; | ||||
| import { ServerDashboardComponent } from "./server-dashboard/server-dashboard.component"; | ||||
| import { ProfileComponent } from "./profile/profile.component"; | ||||
| import { MembersComponent } from "./members/members.component"; | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   { path: '', component: ServerDashboardComponent }, | ||||
|   { path: 'profile', component: ProfileComponent }, | ||||
|   { path: 'members', component: MembersComponent }, | ||||
| ]; | ||||
|  | ||||
| @NgModule({ | ||||
|   | ||||
| @@ -4,13 +4,15 @@ import { ServerDashboardComponent } from './server-dashboard/server-dashboard.co | ||||
| import { ServerRoutingModule } from './server-routing.module'; | ||||
| import { SharedModule } from '../../shared/shared.module'; | ||||
| import { ProfileComponent } from './profile/profile.component'; | ||||
| import { MembersComponent } from './members/members.component'; | ||||
|  | ||||
|  | ||||
|  | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
|     ServerDashboardComponent, | ||||
|     ProfileComponent | ||||
|     ProfileComponent, | ||||
|     MembersComponent | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
|   | ||||
| @@ -37,4 +37,14 @@ export class DataService { | ||||
|       .pipe(map((d) => f ? f(d) : d)); | ||||
|   } | ||||
|  | ||||
|   public mutation<T>(query: string, variables?: object, f?: Function): Observable<T> { | ||||
|     return this.http | ||||
|       .post<{ data: T }>(`${this.appsettings.getApiURL()}/api/graphql`, { | ||||
|         query: query, | ||||
|         variables: variables | ||||
|       }) | ||||
|       .pipe(map((d) => d.data)) | ||||
|       .pipe(map((d) => f ? f(d) : d)); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -65,6 +65,7 @@ | ||||
|         "first_name": "Vorname", | ||||
|         "last_name": "Nachname", | ||||
|         "e_mail": "E-Mail", | ||||
|         "auth_role": "Rolle", | ||||
|         "active": "Aktiv", | ||||
|         "role": "Rolle", | ||||
|         "password": "Passwort", | ||||
| @@ -160,6 +161,29 @@ | ||||
|       "header": "Server", | ||||
|       "dashboard": { | ||||
|         "header": "Server dashboard" | ||||
|       }, | ||||
|       "members": { | ||||
|         "header": "Mitglieder", | ||||
|         "of": "von", | ||||
|         "add": "Hinzufügen", | ||||
|         "reset_filters": "Filter zurücksetzen", | ||||
|         "members": "Mitgliedern", | ||||
|         "headers": { | ||||
|           "id": "ID", | ||||
|           "discord_id": "Discord ID", | ||||
|           "name": "Name", | ||||
|           "xp": "XP", | ||||
|           "ontime": "Ontime", | ||||
|           "level": "Level", | ||||
|           "actions": "Aktionen" | ||||
|         }, | ||||
|         "no_entries_found": "Keine Einträge gefunden", | ||||
|         "message": { | ||||
|           "user_changed": "Benutzer geändert", | ||||
|           "user_changed_d": "Benutzer {{name}} erfolgreich geändert", | ||||
|           "user_change_failed": "Benutzer änderung fehlgeschlagen", | ||||
|           "user_change_failed_d": "Benutzer {{name}} konnte nicht geändert werden!" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "user-list": {}, | ||||
|   | ||||
| @@ -60,7 +60,7 @@ header { | ||||
|         text-align: right; | ||||
|         flex: 1; | ||||
|         justify-content: flex-end; | ||||
|         margin: 0px 5px 0px 0px; | ||||
|         margin: 0 5px 0 0; | ||||
|     } | ||||
|  | ||||
|     .p-menu-overlay { | ||||
| @@ -122,7 +122,7 @@ header { | ||||
|                         display: flex; | ||||
|                         flex-direction: row; | ||||
|                         flex: 1; | ||||
|                         margin: 1.5px 0px; | ||||
|                         margin: 1.5px 0; | ||||
|                     } | ||||
|  | ||||
|                     .content-column { | ||||
| @@ -147,7 +147,7 @@ header { | ||||
|                     } | ||||
|  | ||||
|                     .content-divider { | ||||
|                         margin: 5px 0px; | ||||
|                         margin: 5px 0; | ||||
|                     } | ||||
|  | ||||
|                     .content-input-field { | ||||
| @@ -165,7 +165,7 @@ header { | ||||
|                     } | ||||
|  | ||||
|                     .input-field-info-text { | ||||
|                         margin: 15px 0px; | ||||
|                         margin: 15px 0; | ||||
|                         width: 100%; | ||||
|                     } | ||||
|  | ||||
| @@ -212,6 +212,10 @@ header { | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     .table-header-small { | ||||
|                         width: 25px; | ||||
|                     } | ||||
|  | ||||
|                     .table-header-actions { | ||||
|                         width: 100px; | ||||
|                     } | ||||
| @@ -260,7 +264,7 @@ header { | ||||
|                                     gap: 10px; | ||||
|  | ||||
|                                     .name { | ||||
|                                         margin: 0px; | ||||
|                                         margin: 0; | ||||
|  | ||||
|                                         justify-content: center; | ||||
|                                         align-items: center; | ||||
| @@ -287,7 +291,7 @@ header { | ||||
| } | ||||
|  | ||||
| .p-dialog-footer { | ||||
|     padding: 0px 20px 20px 20px !important; | ||||
|     padding: 0 20px 20px 20px !important; | ||||
| } | ||||
|  | ||||
| .p-dialog-content { | ||||
| @@ -295,7 +299,7 @@ header { | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         flex: 1; | ||||
|         margin: 1.5px 0px; | ||||
|         margin: 1.5px 0; | ||||
|     } | ||||
|  | ||||
|     .content-column { | ||||
| @@ -320,7 +324,7 @@ header { | ||||
|     } | ||||
|  | ||||
|     .content-divider { | ||||
|         margin: 5px 0px; | ||||
|         margin: 5px 0; | ||||
|     } | ||||
|  | ||||
|     .content-input-field { | ||||
| @@ -332,7 +336,7 @@ header { | ||||
| footer { | ||||
|     width: 100%; | ||||
|     height: $footerHeight; | ||||
|     padding: 0px 10px; | ||||
|     padding: 0 10px; | ||||
|  | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
| @@ -347,7 +351,7 @@ footer { | ||||
|         // } | ||||
|  | ||||
|         .version-divider { | ||||
|             margin: 0px 5px; | ||||
|             margin: 0 5px; | ||||
|         } | ||||
|  | ||||
|         // .backend-version { | ||||
| @@ -394,7 +398,7 @@ footer { | ||||
|             } | ||||
|  | ||||
|             .input-field-info-text { | ||||
|                 margin: 15px 0px; | ||||
|                 margin: 15px 0; | ||||
|                 width: 100%; | ||||
|             } | ||||
|  | ||||
| @@ -435,7 +439,7 @@ footer { | ||||
| } | ||||
|  | ||||
| .input-field { | ||||
|     margin: 15px 0px; | ||||
|     margin: 15px 0; | ||||
|  | ||||
|     input, | ||||
|     .p-password { | ||||
|   | ||||
| @@ -11,15 +11,15 @@ | ||||
|   background: none !important; | ||||
|   border: none !important; | ||||
|   width: auto !important; | ||||
|   border-radius: 0px !important; | ||||
|   border-radius: 0 !important; | ||||
|   padding: 0 !important; | ||||
|  | ||||
|   .p-menuitem-link, | ||||
|   .p-panelmenu-header > a, | ||||
|   .p-panelmenu-content .p-menuitem .p-menuitem-link { | ||||
|     $distance: 10px; | ||||
|     padding: $distance 0px $distance $distance !important; | ||||
|     margin: 4px 0px 4px 6px !important; | ||||
|     padding: $distance 0 $distance $distance !important; | ||||
|     margin: 4px 0 4px 6px !important; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -44,8 +44,8 @@ header, | ||||
|  | ||||
|   .p-panelmenu-header > a { | ||||
|     border: none !important; | ||||
|     border-radius: none !important; | ||||
|     font-weight: none !important; | ||||
|     border-radius: unset !important; | ||||
|     font-weight: unset !important; | ||||
|     transition: none !important; | ||||
|   } | ||||
|  | ||||
| @@ -71,8 +71,12 @@ ui-menu .ui-menu-parent .ui-menu-child { | ||||
|   box-shadow: none !important; | ||||
| } | ||||
|  | ||||
| .p-datatable > .p-datatable-wrapper { | ||||
|   overflow: visible !important; | ||||
| } | ||||
|  | ||||
| .p-password { | ||||
|   padding: 0px !important; | ||||
|   padding: 0 !important; | ||||
| } | ||||
|  | ||||
| .p-paginator { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user