#410 #439

Merged
edraft merged 10 commits from #410 into dev 2023-11-19 12:57:44 +01:00
26 changed files with 1107 additions and 41 deletions
Showing only changes of commit a3ebd07093 - Show all commits

View File

@ -27,3 +27,4 @@ class FeatureFlagsEnum(Enum):
short_role_name = "ShortRoleName"
technician_full_access = "TechnicianFullAccess"
steam_special_offers = "SteamSpecialOffers"
scheduled_events = "ScheduledEvents"

View File

@ -29,6 +29,7 @@ class FeatureFlagsSettings(ConfigurationModelABC):
FeatureFlagsEnum.short_role_name.value: False, # 28.09.2023 #378
FeatureFlagsEnum.technician_full_access.value: False, # 03.10.2023 #393
FeatureFlagsEnum.steam_special_offers.value: False, # 11.10.2023 #188
FeatureFlagsEnum.scheduled_events.value: False, # 14.11.2023 #410
}
def __init__(self, **kwargs: dict):

View File

@ -14,6 +14,7 @@ from bot_data.abc.data_seeder_abc import DataSeederABC
from bot_data.abc.game_server_repository_abc import GameServerRepositoryABC
from bot_data.abc.known_user_repository_abc import KnownUserRepositoryABC
from bot_data.abc.level_repository_abc import LevelRepositoryABC
from bot_data.abc.scheduled_event_repository_abc import ScheduledEventRepositoryABC
from bot_data.abc.server_config_repository_abc import ServerConfigRepositoryABC
from bot_data.abc.server_repository_abc import ServerRepositoryABC
from bot_data.abc.short_role_name_repository_abc import ShortRoleNameRepositoryABC
@ -45,6 +46,7 @@ from bot_data.service.client_repository_service import ClientRepositoryService
from bot_data.service.game_server_repository_service import GameServerRepositoryService
from bot_data.service.known_user_repository_service import KnownUserRepositoryService
from bot_data.service.level_repository_service import LevelRepositoryService
from bot_data.service.scheduled_event_repository_service import ScheduledEventRepositoryService
from bot_data.service.seeder_service import SeederService
from bot_data.service.server_config_repository_service import (
ServerConfigRepositoryService,
@ -115,6 +117,7 @@ class DataModule(ModuleABC):
services.add_transient(ServerConfigRepositoryABC, ServerConfigRepositoryService)
services.add_transient(ShortRoleNameRepositoryABC, ShortRoleNameRepositoryService)
services.add_transient(SteamSpecialOfferRepositoryABC, SteamSpecialOfferRepositoryService)
services.add_transient(ScheduledEventRepositoryABC, ScheduledEventRepositoryService)
services.add_transient(SeederService)
services.add_transient(DataSeederABC, TechnicianConfigSeeder)

View File

@ -0,0 +1,34 @@
import { DataWithHistory } from "./data.model";
import { Server, ServerFilter } from "./server.model";
export enum EventType {
stageInstance = 1,
voice = 2,
external = 3,
}
export interface ScheduledEvent extends DataWithHistory {
id?: number;
interval?: string;
name?: string;
description?: string;
channelId?: string;
startTime?: string;
endTime?: string;
entityType?: EventType;
location?: string;
server?: Server;
}
export interface ScheduledEventFilter {
id?: number;
interval?: string;
name?: string;
description?: string;
channelId?: string;
startTime?: string;
endTime?: string;
entityType?: number;
location?: string;
server?: ServerFilter;
}

View File

@ -173,6 +173,70 @@ export class Mutations {
}
`;
static createScheduledEvent = `
mutation createScheduledEvent($interval: String,$name: String,$description: String,$channelId: String,$startTime: String, $endTime: String,$entityType: Int,$location: String, $serverId: ID) {
scheduledEvent {
createScheduledEvent(input: {
interval: $interval,
name: $name,
description: $description,
channelId: $channelId,
startTime: $startTime,
endTime: $endTime,
entityType: $entityType,
location: $location,
serverId: $serverId}
) {
id
name
description
attribute
operator
value
server {
id
}
}
}
}
`;
static updateScheduledEvent = `
mutation updateScheduledEvent($interval: String,$name: String,$description: String,$channelId: String,$startTime: String, $endTime: String,$entityType: Int,$location: String, $serverId: ID) {
scheduledEvent {
updateScheduledEvent(input: {
interval: $interval,
name: $name,
description: $description,
channelId: $channelId,
startTime: $startTime,
endTime: $endTime,
entityType: $entityType,
location: $location,
serverId: $serverId}
) {
id
name
description
attribute
operator
value
}
}
}
`;
static deleteScheduledEvent = `
mutation deleteScheduledEvent($id: ID) {
scheduledEvent {
deleteScheduledEvent(id: $id) {
id
name
}
}
}
`;
static createShortRoleName = `
mutation createShortRoleName($shortName: String, $roleId: String, $position: String, $serverId: ID) {
shortRoleName {

View File

@ -229,6 +229,58 @@ export class Queries {
}
`;
static scheduledEventQuery = `
query ScheduledEventList($serverId: ID, $filter: ScheduledEventFilter, $page: Page, $sort: Sort) {
servers(filter: {id: $serverId}) {
scheduledEventCount
scheduledEvents(filter: $filter, page: $page, sort: $sort) {
id
interval
name
description
channelId
startTime
endTime
entityType
location
server {
id
name
}
createdAt
modifiedAt
}
}
}
`;
static scheduledEventWithHistoryQuery = `
query ScheduledEventHistory($serverId: ID, $id: ID) {
servers(filter: {id: $serverId}) {
scheduledEventCount
scheduledEvents(filter: {id: $id}) {
id
history {
id
interval
name
description
channelId
startTime
endTime
entityType
location
server
deleted
dateFrom
dateTo
}
}
}
}
`;
static shortRoleNamePositionsQuery = `
query {

View File

@ -9,6 +9,7 @@ import { ServerConfig } from "../config/server-config.model";
import { ShortRoleName } from "../data/short_role_name.model";
import { FeatureFlag } from "../config/feature-flags.model";
import { UserWarning } from "../data/user_warning.model";
import { ScheduledEvent } from "../data/scheduled_events.model";
export interface Query {
serverCount: number;
@ -57,6 +58,11 @@ export interface AchievementListQuery {
achievements: Achievement[];
}
export interface ScheduledEventListQuery {
scheduledEventCount: number;
scheduledEvents: ScheduledEvent[];
}
export interface AutoRoleQuery {
autoRoleCount: number;
autoRoles: AutoRole[];

View File

@ -7,6 +7,7 @@ import { TechnicianConfig } from "../config/technician-config.model";
import { ServerConfig } from "../config/server-config.model";
import { ShortRoleName } from "../data/short_role_name.model";
import { UserWarning } from "../data/user_warning.model";
import { ScheduledEvent } from "../data/scheduled_events.model";
export interface GraphQLResult {
data: {
@ -71,6 +72,14 @@ export interface AchievementMutationResult {
};
}
export interface ScheduledEventMutationResult {
scheduledEvent: {
createScheduledEvent?: ScheduledEvent
updateScheduledEvent?: ScheduledEvent
deleteScheduledEvent?: ScheduledEvent
};
}
export interface ShortRoleNameMutationResult {
shortRoleName: {
createShortRoleName?: ShortRoleName

View File

@ -13,7 +13,7 @@ import { InputTextModule } from "primeng/inputtext";
import { MenuModule } from "primeng/menu";
import { PasswordModule } from "primeng/password";
import { ProgressSpinnerModule } from "primeng/progressspinner";
import { SortableColumn, TableModule } from "primeng/table";
import { TableModule } from "primeng/table";
import { ToastModule } from "primeng/toast";
import { AuthRolePipe } from "./pipes/auth-role.pipe";
import { IpAddressPipe } from "./pipes/ip-address.pipe";
@ -24,18 +24,20 @@ import { InputNumberModule } from "primeng/inputnumber";
import { ImageModule } from "primeng/image";
import { SidebarModule } from "primeng/sidebar";
import { HistoryBtnComponent } from "./components/history-btn/history-btn.component";
import { DataViewModule, DataViewLayoutOptions } from "primeng/dataview";
import { DataViewLayoutOptions, DataViewModule } from "primeng/dataview";
import { ConfigListComponent } from "./components/config-list/config-list.component";
import { MultiSelectModule } from "primeng/multiselect";
import { HideableColumnComponent } from './components/hideable-column/hideable-column.component';
import { HideableHeaderComponent } from './components/hideable-header/hideable-header.component';
import { MultiSelectColumnsComponent } from './base/multi-select-columns/multi-select-columns.component';
import { FeatureFlagListComponent } from './components/feature-flag-list/feature-flag-list.component';
import { HideableColumnComponent } from "./components/hideable-column/hideable-column.component";
import { HideableHeaderComponent } from "./components/hideable-header/hideable-header.component";
import { MultiSelectColumnsComponent } from "./base/multi-select-columns/multi-select-columns.component";
import { FeatureFlagListComponent } from "./components/feature-flag-list/feature-flag-list.component";
import { InputSwitchModule } from "primeng/inputswitch";
import { CalendarModule } from "primeng/calendar";
import { DataImportAndExportComponent } from './components/data-import-and-export/data-import-and-export.component';
import { DataImportAndExportComponent } from "./components/data-import-and-export/data-import-and-export.component";
import { FileUploadModule } from "primeng/fileupload";
import { SelectButtonModule } from "primeng/selectbutton";
import { TabViewModule } from "primeng/tabview";
import { RadioButtonModule } from "primeng/radiobutton";
const PrimeNGModules = [
@ -66,7 +68,9 @@ const PrimeNGModules = [
CalendarModule,
FileUploadModule,
SelectButtonModule,
]
TabViewModule,
RadioButtonModule
];
@NgModule({
declarations: [
@ -79,7 +83,7 @@ const PrimeNGModules = [
HideableHeaderComponent,
MultiSelectColumnsComponent,
FeatureFlagListComponent,
DataImportAndExportComponent,
DataImportAndExportComponent
],
imports: [
CommonModule,

View File

@ -0,0 +1,44 @@
<div class="edit-dialog">
<p-dialog [header]="header" [(visible)]="visible" [modal]="true"
[draggable]="false" [resizable]="false"
[style]="{ width: '50vw' }">
<form [formGroup]="inputForm">
<p-tabView [(activeIndex)]="activeIndex">
<p-tabPanel header="{{'view.server.scheduled_events.edit_dialog.location.tab_name' | translate}}">
<div *ngFor="let enum of EventType | keyvalue" class="field-checkbox">
<p-radioButton [inputId]="enum.key" [value]="enum.value" formControlName="entityType"></p-radioButton>
<label [for]="enum.key"
class="ml-2">{{'view.server.scheduled_events.edit_dialog.location.' + enum.key | translate}}</label>
</div>
</p-tabPanel>
<p-tabPanel header="{{'view.server.scheduled_events.edit_dialog.event_info.tab_name' | translate}}">
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem
aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
Nemo
enim
ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos
qui
ratione voluptatem sequi nesciunt. Consectetur, adipisci velit, sed quia non numquam eius modi.
</p>
</p-tabPanel>
</p-tabView>
<ng-template pTemplate="footer">
<div style="display: flex;">
<div class="btn-wrapper" style="flex: 1;">
<button pButton label="{{'common.back' | translate}}" class="text-btn"
(click)="back()" [disabled]="activeIndex == 0"></button>
</div>
<div class="btn-wrapper">
<button pButton label="{{'common.abort' | translate}}" class="btn danger-btn"
(click)="visible = false"></button>
<button *ngIf="activeIndex < 1" pButton label="{{'common.continue' | translate}}" class="btn"
(click)="next()"></button>
<button *ngIf="activeIndex == 1" pButton label="{{'common.save' | translate}}" class="btn"
(click)="saveEvent()" [disabled]="!inputForm.valid"></button>
</div>
</div>
</ng-template>
</form>
</p-dialog>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditScheduledEventDialogComponent } from './edit-scheduled-event-dialog.component';
describe('EditScheduledEventDialogComponent', () => {
let component: EditScheduledEventDialogComponent;
let fixture: ComponentFixture<EditScheduledEventDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EditScheduledEventDialogComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(EditScheduledEventDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,70 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { EventType, ScheduledEvent } from "../../../../../../models/data/scheduled_events.model";
import { TranslateService } from "@ngx-translate/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";
@Component({
selector: "app-edit-scheduled-event-dialog",
templateUrl: "./edit-scheduled-event-dialog.component.html",
styleUrls: ["./edit-scheduled-event-dialog.component.scss"]
})
export class EditScheduledEventDialogComponent {
@Input() event?: ScheduledEvent;
@Output() save = new EventEmitter<ScheduledEvent>();
get visible() {
return this.event != undefined;
}
set visible(val: boolean) {
if (!val) {
this.event = undefined;
}
this.visible = val;
}
get header() {
if (this.event && this.event.createdAt === "" && this.event.modifiedAt === "") {
return this.translate.instant("view.server.scheduled_events.edit_dialog.add_header");
} else {
return this.translate.instant("view.server.scheduled_events.edit_dialog.edit_header");
}
}
public activeIndex: number = 0;
public inputForm = this.fb.group({
id: new FormControl<number | undefined>(this.event?.id),
interval: new FormControl<string | undefined>(this.event?.interval, [Validators.required]),
entityType: new FormControl<number | undefined>(this.event?.entityType, [Validators.required]),
channelId: new FormControl<string | undefined>(this.event?.channelId, this.event?.entityType == EventType.voice || this.event?.entityType == EventType.stageInstance ? [Validators.required] : []),
location: new FormControl<string | undefined>(this.event?.location, this.event?.entityType == EventType.external ? [Validators.required] : []),
name: new FormControl<string | undefined>(this.event?.name, [Validators.required]),
startTime: new FormControl<string | undefined>(this.event?.startTime, [Validators.required]),
endTime: new FormControl<string | undefined>(this.event?.endTime),
description: new FormControl<string | undefined>(this.event?.description)
});
constructor(
private translate: TranslateService,
private fb: FormBuilder
) {
}
public saveEvent() {
this.save.emit(this.event);
}
public next() {
this.activeIndex++;
}
public back() {
this.activeIndex--;
}
protected readonly EventType = {
stage: EventType.stageInstance,
voice: EventType.voice,
somewhere_else: EventType.external
};
}

View File

@ -0,0 +1,307 @@
<app-edit-scheduled-event-dialog [event]="editableScheduledEvent" (save)="onRowEditSave($event)"></app-edit-scheduled-event-dialog>
<h1>
{{'view.server.scheduled_events.header' | translate}}
</h1>
<div class="content-wrapper">
<div class="content">
<p-table #dt [value]="scheduledEvents" [responsive]="true" responsiveLayout="stack" [breakpoint]="'720px'"
dataKey="id" [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-table-info">
<div class="table-caption-text">
<ng-container *ngIf="!loading">{{scheduledEvents.length}} {{'common.of' | translate}}
{{dt.totalRecords}}
</ng-container>
{{'view.server.scheduled_events.scheduled_events' | translate}}
</div>
<app-multi-select-columns [table]="name" [columns]="columns"
[(hiddenColumns)]="hiddenColumns"></app-multi-select-columns>
</div>
<div class="table-caption-btn-wrapper btn-wrapper">
<button pButton label="{{'common.add' | translate}}" class="icon-btn btn"
icon="pi pi-plus" (click)="addScheduledEvent(dt)"
[disabled]="isEditingNew || !user?.isModerator && !user?.isAdmin">
</button>
<button pButton label="{{'common.reset_filters' | translate}}" icon="pi pi-undo"
class="icon-btn btn" (click)="resetFilters()">
</button>
<app-data-import-and-export name="scheduledEvent" [(data)]="scheduledEvents"
[callback]="callback" [validator]="validator"></app-data-import-and-export>
</div>
</div>
</ng-template>
<ng-template pTemplate="header">
<tr>
<th hideable-th="id" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.id' | translate}}</div>
<p-sortIcon field="id" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="interval" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.interval' | translate}}</div>
<p-sortIcon field="interval" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="name" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.name' | translate}}</div>
<p-sortIcon field="name" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="description" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.description' | translate}}</div>
<p-sortIcon field="description" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="channel_id" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.channel_id' | translate}}</div>
<p-sortIcon field="channelId" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="start_time" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.start_time' | translate}}</div>
<p-sortIcon field="startTime" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="end_time" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.end_time' | translate}}</div>
<p-sortIcon field="endTime" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="type" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.type' | translate}}</div>
<p-sortIcon field="entityType" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th hideable-th="location" [parent]="this" [sortable]="true">
<div class="table-header-label">
<div class="table-header-text">{{'common.location' | translate}}</div>
<p-sortIcon field="location" class="table-header-icon"></p-sortIcon>
</div>
</th>
<th>
<div class="table-header-label">
<div class="table-header-text">{{'common.created_at' | translate}}</div>
</div>
</th>
<th>
<div class="table-header-label">
<div class="table-header-text">{{'common.modified_at' | translate}}</div>
</div>
</th>
<th>
<div class="table-header-label">
<div class="table-header-text">{{'common.actions' | translate}}</div>
</div>
</th>
</tr>
<tr>
<th hideable-th="id" [parent]="this" class="table-header-small">
<form [formGroup]="filterForm">
<input type="text" pInputText formControlName="id"
placeholder="{{'common.id' | translate}}">
</form>
</th>
<th class="table-header-small"></th>
<th hideable-th="name" [parent]="this">
<form [formGroup]="filterForm">
<input type="text" pInputText formControlName="name"
placeholder="{{'common.name' | translate}}">
</form>
</th>
<th class="table-header-small"></th>
<th class="table-header-small"></th>
<th class="table-header-small"></th>
<th class="table-header-small"></th>
<th class="table-header-small"></th>
<th class="table-header-small"></th>
<th class="table-header-small-dropdown"></th>
<th class="table-header-small-dropdown"></th>
<th class="table-header-actions"></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-scheduledEvent let-editing="editing" let-ri="rowIndex">
<tr [pEditableRow]="scheduledEvent">
<td hideable-td="id" [parent]="this">
<span class="p-column-title">{{'common.id' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
{{scheduledEvent.id}}
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.id}}
</ng-template>
</p-cellEditor>
</td>
<td hideable-td="interval" [parent]="this">
<span class="p-column-title">{{'common.interval' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="scheduledEvent.interval">
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.interval}}
</ng-template>
</p-cellEditor>
</td>
<td hideable-td="name" [parent]="this">
<span class="p-column-title">{{'common.name' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="scheduledEvent.name">
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.name}}
</ng-template>
</p-cellEditor>
</td>
<td hideable-td="description" [parent]="this">
<span class="p-column-title">{{'common.description' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="scheduledEvent.description">
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.description}}
</ng-template>
</p-cellEditor>
</td>
<td hideable-td="channel_id" [parent]="this">
<span class="p-column-title">{{'common.channel_id' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="scheduledEvent.channel_id">
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.channel_id}}
</ng-template>
</p-cellEditor>
</td>
<td hideable-td="start_time" [parent]="this">
<span class="p-column-title">{{'common.start_time' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="scheduledEvent.start_time">
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.start_time}}
</ng-template>
</p-cellEditor>
</td>
<td hideable-td="end_time" [parent]="this">
<span class="p-column-title">{{'common.end_time' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="scheduledEvent.end_time">
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.end_time}}
</ng-template>
</p-cellEditor>
</td>
<td hideable-td="type" [parent]="this">
<span class="p-column-title">{{'common.type' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="scheduledEvent.type">
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.type}}
</ng-template>
</p-cellEditor>
</td>
<td hideable-td="location" [parent]="this">
<span class="p-column-title">{{'common.location' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
<input class="table-edit-input" pInputText type="text" [(ngModel)]="scheduledEvent.location">
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.location}}
</ng-template>
</p-cellEditor>
</td>
<td>
<span class="p-column-title">{{'common.created_at' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
{{scheduledEvent.createdAt | date:'dd.MM.yy HH:mm'}}
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.createdAt | date:'dd.MM.yy HH:mm'}}
</ng-template>
</p-cellEditor>
</td>
<td>
<span class="p-column-title">{{'common.modified_at' | translate}}:</span>
<p-cellEditor>
<ng-template pTemplate="input">
{{scheduledEvent.modifiedAt | date:'dd.MM.yy HH:mm'}}
</ng-template>
<ng-template pTemplate="output">
{{scheduledEvent.modifiedAt | date:'dd.MM.yy HH:mm'}}
</ng-template>
</p-cellEditor>
</td>
<td>
<div class="btn-wrapper">
<app-history-btn *ngIf="!isEditingNew" [id]="scheduledEvent.id" [query]="query"
translationKey="view.server.scheduledEvent.header"></app-history-btn>
<button *ngIf="!editing" pButton class="btn icon-btn" icon="pi pi-pencil"
(click)="onRowEditInit(dt, scheduledEvent, ri)"
[disabled]="!user || !user.isModerator && !user.isAdmin"></button>
<button *ngIf="!editing" pButton class="btn icon-btn danger-icon-btn" icon="pi pi-trash"
(click)="deleteScheduledEvent(scheduledEvent)"
[disabled]="!user || !user.isModerator && !user.isAdmin"></button>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr></tr>
<tr>
<td colspan="14">{{'common.no_entries_found' | translate}}</td>
</tr>
<tr></tr>
</ng-template>
<ng-template pTemplate="paginatorleft">
</ng-template>
</p-table>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ScheduledEventsComponent } from './scheduled-events.component';
describe('ScheduledEventsComponent', () => {
let component: ScheduledEventsComponent;
let fixture: ComponentFixture<ScheduledEventsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ScheduledEventsComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ScheduledEventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,287 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, FormGroup } from "@angular/forms";
import { Page } from "../../../../../../models/graphql/filter/page.model";
import { Sort, SortDirection } from "../../../../../../models/graphql/filter/sort.model";
import { Subject, throwError } from "rxjs";
import { Server } from "../../../../../../models/data/server.model";
import { UserDTO } from "../../../../../../models/auth/auth-user.dto";
import { LazyLoadEvent } from "primeng/api";
import { Queries } from "../../../../../../models/graphql/queries.model";
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 { DataService } from "../../../../../../services/data/data.service";
import { SidebarService } from "../../../../../../services/sidebar/sidebar.service";
import { ActivatedRoute } from "@angular/router";
import { Query, ScheduledEventListQuery } from "../../../../../../models/graphql/query.model";
import { catchError, debounceTime, takeUntil } from "rxjs/operators";
import { Table } from "primeng/table";
import { ScheduledEventMutationResult } from "../../../../../../models/graphql/result.model";
import { Mutations } from "../../../../../../models/graphql/mutations.model";
import { ComponentWithTable } from "../../../../../../base/component-with-table";
import { EventType, ScheduledEvent, ScheduledEventFilter } from "../../../../../../models/data/scheduled_events.model";
@Component({
selector: "app-scheduled-events",
templateUrl: "./scheduled-events.component.html",
styleUrls: ["./scheduled-events.component.scss"]
})
export class ScheduledEventsComponent extends ComponentWithTable implements OnInit, OnDestroy {
public scheduledEvents: ScheduledEvent[] = [];
public loading = true;
public filterForm!: FormGroup<{
id: FormControl<number | null>,
interval: FormControl<string | null>,
name: FormControl<string | null>,
description: FormControl<string | null>,
channelId: FormControl<string | null>,
startTime: FormControl<string | null>,
endTime: FormControl<string | null>,
entityType: FormControl<EventType | null>,
location: FormControl<string | null>,
}>;
public filter: ScheduledEventFilter = {};
public page: Page = {
pageSize: undefined,
pageIndex: undefined
};
public sort: Sort = {
sortColumn: undefined,
sortDirection: undefined
};
public totalRecords: number = 0;
private unsubscriber = new Subject<void>();
private server: Server = {};
public user: UserDTO | null = null;
public query: string = Queries.scheduledEventWithHistoryQuery;
public editableScheduledEvent?: ScheduledEvent = undefined;
public 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,
private route: ActivatedRoute) {
super("ScheduledEvent", ["id", "interval", "name", "description", "channel_id", "start_time", "end_time", "type", "location"],
(oldElement: ScheduledEvent, newElement: ScheduledEvent) => {
return oldElement.name === newElement.name;
});
}
public ngOnInit(): void {
this.loading = true;
this.setFilterForm();
this.data.getServerFromRoute(this.route).then(async server => {
this.server = server;
let authUser = await this.authService.getLoggedInUser();
this.user = authUser?.users?.find(u => u.server == this.server.id) ?? null;
});
}
public ngOnDestroy(): void {
this.unsubscriber.next();
this.unsubscriber.complete();
}
public loadNextPage(): void {
this.data.query<ScheduledEventListQuery>(Queries.scheduledEventQuery, {
serverId: this.server.id, filter: this.filter, page: this.page, sort: this.sort
},
(data: Query) => {
return data.servers[0];
}
).subscribe(data => {
this.totalRecords = data.scheduledEventCount;
this.scheduledEvents = data.scheduledEvents;
this.spinner.hideSpinner();
this.loading = false;
});
}
public setFilterForm(): void {
this.filterForm = this.fb.group({
id: new FormControl<number | null>(null),
interval: new FormControl<string | null>(null),
name: new FormControl<string | null>(null),
description: new FormControl<string | null>(null),
channelId: new FormControl<string | null>(null),
startTime: new FormControl<string | null>(null),
endTime: new FormControl<string | null>(null),
entityType: new FormControl<EventType | null>(null),
location: new FormControl<string | null>(null),
});
this.filterForm.valueChanges.pipe(
takeUntil(this.unsubscriber),
debounceTime(600)
).subscribe(changes => {
if (changes.id) {
this.filter.id = changes.id;
} else {
this.filter.id = undefined;
}
if (changes.interval) {
this.filter.interval = changes.interval;
} else {
this.filter.interval = undefined;
}
if (changes.name) {
this.filter.name = changes.name;
} else {
this.filter.name = undefined;
}
if (changes.description) {
this.filter.description = changes.description;
} else {
this.filter.description = undefined;
}
if (changes.channelId) {
this.filter.channelId = changes.channelId;
} else {
this.filter.channelId = undefined;
}
if (changes.startTime) {
this.filter.startTime = changes.startTime;
} else {
this.filter.startTime = undefined;
}
if (changes.endTime) {
this.filter.endTime = changes.endTime;
} else {
this.filter.endTime = undefined;
}
if (changes.entityType) {
this.filter.entityType = changes.entityType;
} else {
this.filter.entityType = undefined;
}
if (changes.location) {
this.filter.location = changes.location;
} else {
this.filter.location = undefined;
}
if (this.page.pageSize)
this.page.pageSize = 10;
if (this.page.pageIndex)
this.page.pageIndex = 0;
this.loadNextPage();
});
}
public newScheduledEventTemplate: ScheduledEvent = {
createdAt: "",
modifiedAt: ""
};
public nextPage(event: LazyLoadEvent): void {
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 ? SortDirection.ASC : event.sortOrder === -1 ? SortDirection.DESC : SortDirection.ASC;
this.loadNextPage();
}
public resetFilters(): void {
this.filterForm.reset();
}
public onRowEditInit(table: Table, event: ScheduledEvent, index: number): void {
this.editableScheduledEvent = event;
}
public override onRowEditSave(newScheduledEvent: ScheduledEvent): void {
if (this.isEditingNew && JSON.stringify(newScheduledEvent) === JSON.stringify(this.newScheduledEventTemplate)) {
this.isEditingNew = false;
return;
}
if (this.isEditingNew) {
this.spinner.showSpinner();
this.data.mutation<ScheduledEventMutationResult>(Mutations.createScheduledEvent, {
name: newScheduledEvent.name,
serverId: this.server.id
}
).pipe(catchError(err => {
this.isEditingNew = false;
this.spinner.hideSpinner();
return throwError(err);
})).subscribe(result => {
this.isEditingNew = false;
this.spinner.hideSpinner();
this.toastService.success(this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_create"), this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_create_d", { name: result.scheduledEvent.createScheduledEvent?.name }));
this.loadNextPage();
});
return;
}
this.spinner.showSpinner();
this.data.mutation<ScheduledEventMutationResult>(Mutations.updateScheduledEvent, {
id: newScheduledEvent.id,
name: newScheduledEvent.name,
}
).pipe(catchError(err => {
this.spinner.hideSpinner();
return throwError(err);
})).subscribe(_ => {
this.spinner.hideSpinner();
this.toastService.success(this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_update"), this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_update_d", { name: newScheduledEvent.name }));
this.loadNextPage();
});
}
public deleteScheduledEvent(ScheduledEvent: ScheduledEvent): void {
this.confirmDialog.confirmDialog(
this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_delete"), this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_delete_q", { name: ScheduledEvent.name }),
() => {
this.spinner.showSpinner();
this.data.mutation<ScheduledEventMutationResult>(Mutations.deleteScheduledEvent, {
id: ScheduledEvent.id
}
).pipe(catchError(err => {
this.spinner.hideSpinner();
return throwError(err);
})).subscribe(l => {
this.spinner.hideSpinner();
this.toastService.success(this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_deleted"), this.translate.instant("view.server.ScheduledEvents.message.scheduled_event_deleted_d", { name: ScheduledEvent.name }));
this.loadNextPage();
});
});
}
public addScheduledEvent(table: Table): void {
this.editableScheduledEvent = JSON.parse(JSON.stringify(this.newScheduledEventTemplate));
// const newScheduledEvent = JSON.parse(JSON.stringify(this.newScheduledEventTemplate));
//
// this.scheduledEvents = [newScheduledEvent, ...this.scheduledEvents];
//
// table.initRowEdit(newScheduledEvent);
//
// const index = this.scheduledEvents.findIndex(l => l.id == newScheduledEvent.id);
// this.onRowEditInit(table, newScheduledEvent, index);
//
// this.isEditingNew = true;
}
}

View File

@ -0,0 +1,14 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { ScheduledEventsComponent } from "./components/scheduled-events/scheduled-events.component";
const routes: Routes = [
{ path: "", component: ScheduledEventsComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ScheduledEventsRoutingModule {
}

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ScheduledEventsRoutingModule } from './scheduled-events-routing.module';
import { ScheduledEventsComponent } from "./components/scheduled-events/scheduled-events.component";
import { SharedModule } from "../../../shared/shared.module";
import { EditScheduledEventDialogComponent } from './components/edit-scheduled-event-dialog/edit-scheduled-event-dialog.component';
@NgModule({
declarations: [
ScheduledEventsComponent,
EditScheduledEventDialogComponent
],
imports: [
CommonModule,
SharedModule,
ScheduledEventsRoutingModule
]
})
export class ScheduledEventsModule { }

View File

@ -19,6 +19,7 @@ const routes: Routes = [
{ path: "levels", loadChildren: () => import("./levels/levels.module").then(m => m.LevelsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } },
{ path: "achievements", loadChildren: () => import("./achievements/achievements.module").then(m => m.AchievementsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } },
{ path: "short-role-names", loadChildren: () => import("./short-role-name/short-role-name.module").then(m => m.ShortRoleNameModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } },
{ path: "scheduled-events", loadChildren: () => import("./scheduled-events/scheduled-events.module").then(m => m.ScheduledEventsModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Moderator } },
{ path: "config", loadChildren: () => import("./config/config.module").then(m => m.ConfigModule), canActivate: [AuthGuard], data: { memberRole: MemberRoles.Admin } }
];

View File

@ -1,14 +1,8 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ShortRoleNamesComponent } from './components/short-role-names/short-role-names.component';
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ShortRoleNamesComponent } from "./components/short-role-names/short-role-names.component";
import { ShortRoleNameRoutingModule } from "./short-role-name-routing.module";
import { ButtonModule } from "primeng/button";
import { InputTextModule } from "primeng/inputtext";
import { ReactiveFormsModule } from "@angular/forms";
import { SharedModule } from "../../../shared/shared.module";
import { TableModule } from "primeng/table";
import { TranslateModule } from "@ngx-translate/core";
@NgModule({
@ -18,13 +12,7 @@ import { TranslateModule } from "@ngx-translate/core";
imports: [
CommonModule,
ShortRoleNameRoutingModule,
ButtonModule,
InputTextModule,
ReactiveFormsModule,
SharedModule,
SharedModule,
TableModule,
TranslateModule
]
})
export class ShortRoleNameModule { }

View File

@ -30,6 +30,7 @@ export class SidebarService {
serverAutoRoles: MenuItem = {};
serverLevels: MenuItem = {};
serverAchievements: MenuItem = {};
serverScheduledEvents: MenuItem = {};
serverShortRoleNames: MenuItem = {};
serverConfig: MenuItem = {};
serverMenu: MenuItem = {};
@ -110,6 +111,13 @@ export class SidebarService {
routerLink: `server/${this.server?.id}/achievements`
};
this.serverScheduledEvents = {
label: this.isSidebarOpen ? this.translateService.instant("sidebar.server.scheduled_events") : "",
icon: "pi pi-calender",
visible: true,
routerLink: `server/${this.server?.id}/scheduled-events`
};
this.serverShortRoleNames = {
label: this.isSidebarOpen ? this.translateService.instant("sidebar.server.short_role_names") : "",
icon: "pi pi-list",
@ -129,7 +137,7 @@ export class SidebarService {
icon: "pi pi-server",
visible: false,
expanded: true,
items: [this.serverDashboard, this.serverProfile, this.serverMembers, this.serverAutoRoles, this.serverLevels, this.serverAchievements, this.serverShortRoleNames, this.serverConfig]
items: [this.serverDashboard, this.serverProfile, this.serverMembers, this.serverAutoRoles, this.serverLevels, this.serverAchievements, this.serverScheduledEvents, this.serverShortRoleNames, this.serverConfig]
};
this.adminConfig = {
label: this.isSidebarOpen ? this.translateService.instant("sidebar.config") : "",
@ -205,6 +213,7 @@ export class SidebarService {
this.serverAutoRoles.visible = isTechnicianAndFullAccessActive || this.hasFeature("AutoRoleModule") && user?.isModerator;
this.serverLevels.visible = isTechnicianAndFullAccessActive || this.hasFeature("LevelModule") && user?.isModerator;
this.serverAchievements.visible = isTechnicianAndFullAccessActive || this.hasFeature("AchievementsModule") && user?.isModerator;
this.serverScheduledEvents.visible = isTechnicianAndFullAccessActive || this.hasFeature("ScheduledEvents") && user?.isModerator;
this.serverShortRoleNames.visible = isTechnicianAndFullAccessActive || this.hasFeature("ShortRoleName") && user?.isAdmin;
this.serverConfig.visible = isTechnicianAndFullAccessActive || user?.isAdmin;

View File

@ -124,12 +124,14 @@
},
"common": {
"404": "404 - Der Eintrag konnte nicht gefunden werden",
"abort": "Abbrechen",
"actions": "Aktionen",
"active": "Aktiv",
"add": "Hinzufügen",
"attribute": "Attribut",
"auth_role": "Rolle",
"author": "Autor",
"back": "Zurück",
"bool_as_string": {
"false": "Nein",
"true": "Ja"
@ -137,12 +139,14 @@
"channel_id": "Kanal Id",
"channel_name": "Kanal",
"color": "Farbe",
"continue": "Weiter",
"created_at": "Erstellt am",
"description": "Beschreibung",
"discord_id": "Discord Id",
"edit": "Bearbeiten",
"email": "E-Mail",
"emoji": "Emoji",
"end_time": "Endzeit",
"error": "Fehler",
"export": "Exportieren",
"feature_flags": "Funktionen",
@ -176,11 +180,13 @@
},
"id": "Id",
"import": "Importieren",
"interval": "Interval",
"joined_at": "Beigetreten am",
"last_name": "Nachname",
"leaved_at": "Verlassen am",
"left_server": "Aktiv",
"level": "Level",
"location": "Ort",
"message_id": "Nachricht Id",
"min_xp": "Min. XP",
"modified_at": "Bearbeitet am",
@ -200,10 +206,12 @@
"role": "Rolle",
"rule_count": "Regeln",
"save": "Speichern",
"start_time": "Startzeit",
"state": {
"off": "Aus",
"on": "Ein"
},
"type": "Typ",
"user_warnings": "Verwarnungen",
"users": "Benutzer",
"value": "Wert",
@ -538,6 +546,33 @@
"reaction_count": "Anzahl Reaktionen",
"xp": "XP"
},
"scheduled_events": {
"edit_dialog": {
"add_header": "Event hinzufügen",
"edit_header": "Event bearbeiten",
"event_info": {
"description_input": "Erzähl den Leuten ein wenig mehr über dein Event. Markdown, neue Zeilen und Links werden unterstützt.",
"event_topic": "Thema",
"event_topic_input": "Worum geht es bei deinem Event?",
"header": "Worum geht es bei deinem Event?",
"start_date": "Startdatum",
"start_time": "Startzeit",
"tab_name": "Eventinformationen"
},
"location": {
"header": "Wo ist dein Event?",
"somewhere_else": "Irgendwo anders",
"somewhere_else_input": "Ort eingeben",
"stage": "Stage-Kanal",
"stage_input": "Stage-Kanal auswählen",
"tab_name": "Verzeichnis",
"voice": "Sprachkanal",
"voice_input": "Sprachkanal auswählen"
}
},
"header": "Geplante Events",
"scheduled_events": "Geplante Events"
},
"short_role_names": {
"header": "Rollen Kürzel",
"message": {

View File

@ -124,12 +124,14 @@
},
"common": {
"404": "404 - Entry not found!",
"abort": "Abort",
"actions": "Actions",
"active": "Active",
"add": "Add",
"attribute": "Attribute",
"auth_role": "Role",
"author": "Author",
"back": "Back",
"bool_as_string": {
"false": "No",
"true": "Yes"
@ -137,6 +139,7 @@
"channel_id": "Channel Id",
"channel_name": "Channel",
"color": "Color",
"continue": "Continue",
"created_at": "Created at",
"description": "Description",
"discord_id": "Discord Id",
@ -538,6 +541,33 @@
"reaction_count": "Reaction count",
"xp": "XP"
},
"scheduled_events": {
"edit_dialog": {
"add_header": "Add event",
"edit_header": "Edit event",
"event_info": {
"description_input": "Tell people a little more about your event. Markdown, new lines and links are supported.",
"event_topic": "Event topic",
"event_topic_input": "What's your event?",
"header": "What's your event about?",
"start_date": "Start date",
"start_time": "Start time",
"tab_name": "Event info"
},
"location": {
"header": "Where is your event?",
"somewhere_else": "Somewhere else",
"somewhere_else_input": "Enter a location",
"stage": "Stage channel",
"stage_input": "Select a stage channel",
"tab_name": "Location",
"voice": "Voice channel",
"voice_input": "Select a voice channel"
}
},
"header": "Scheduled events",
"scheduled_events": "Scheduled events"
},
"short_role_names": {
"header": "Level",
"message": {

View File

@ -635,6 +635,18 @@ footer {
border: 0;
}
.btn,
.icon-btn,
.text-btn,
.danger-btn,
.danger-icon-btn {
span {
transition-duration: unset !important;
}
transition: none !important;
}
.spinner-component-wrapper {
position: absolute;
top: 0;
@ -661,6 +673,27 @@ p-inputNumber {
border: none !important;
}
.edit-dialog {
.p-dialog-content {
padding: 0 !important;
.p-tabview-nav {
justify-content: space-between;
li {
width: 100%;
a {
width: 100%;
justify-content: center;
}
}
}
}
}
@media (max-width: 720px) {
footer {
.left,

View File

@ -552,6 +552,7 @@
}
}
.danger-btn,
.danger-icon-btn {
background-color: transparent !important;
color: $primaryTextColor !important;
@ -568,22 +569,6 @@
}
}
.danger-btn {
background-color: $primaryErrorColor !important;
color: $primaryErrorColor !important;
border: 0 !important;
&:hover {
background-color: $primaryErrorColor !important;
color: $primaryTextColor !important;
border: 0;
}
.pi {
font-size: 1.275rem !important;
}
}
.p-datatable .p-sortable-column.p-highlight,
.p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon,
.p-datatable .p-sortable-column:not(.p-highlight):hover {
@ -777,4 +762,26 @@
}
}
}
.edit-dialog {
.p-dialog-content {
.p-tabview {
.p-tabview-nav li .p-tabview-nav-link:not(.p-disabled):focus {
box-shadow: none !important;
}
.p-tabview-nav li.p-highlight .p-tabview-nav-link {
color: $primaryHeaderColor !important;
border-color: $primaryHeaderColor !important;
}
.p-tabview-nav,
.p-tabview-nav li .p-tabview-nav-link,
.p-tabview-panels {
background-color: $secondaryBackgroundColor !important;
color: $primaryTextColor !important;
}
}
}
}
}