Skip to content

Commit

Permalink
feat(post): Add support for CS ticket ID field in post editor
Browse files Browse the repository at this point in the history
- Added a new property `supportTicketStatus` to the `PlayerPostEditorDto` interface and implemented it in the `PostEditorComponent`
- Created a new component `CsTicketIdHelpComponent` to display information about Customer Support tickets
- Added a new HTML template file for the `CsTicketIdHelpComponent`
- Updated the HTML template of the `PostEditorComponent` to include a CS ticket ID field when certain conditions are met
- Updated the CSS styles of the `SeedTokenChangeComponent` (removed)
- Updated the TypeScript code of the `SeedTokenChangeComponent` (removed)
- Updated the HTML template of the `PostComponent` to display CS ticket related icons and tooltips

Implements frontend requirements for #165.
  • Loading branch information
SakuraIsayeki committed Sep 1, 2024
1 parent 495032c commit 2d228ab
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 73 deletions.
4 changes: 4 additions & 0 deletions wowskarma.app/src/app/services/api/models/player-post-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export interface PlayerPostDto {
replayState?: ReplayState
title?: null | string;
updatedAt?: null | string;
supportTicketStatus?: {
hasTicket: boolean;
ticketId: number | null;
}
}

enum ReplayState {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<div class="modal-header">
<h4 class="modal-title">Customer Support Tickets</h4>

<div>
<button type="button" class="btn-close mx-2" aria-label="Close" (click)="this.modal.close()"></button>
</div>
</div>

<div class="modal-body d-flex flex-column gap-3">
<div>
<h4>What is a Customer Support Ticket ?</h4>

<p class="text-body">
Wargaming's Customer Support (aka CS or Player Support) helps players with any issue related to the game.
Submitting a ticket to CS is the best way to get help with your issue, may it be a bug, a technical problem, or a report.
</p>
</div>

<div>
<h4>How is this related to WOWS Karma ?</h4>

<p class="text-body">
WOWS Karma is a community-based platform that is separate from the Customer Support system.
While endorsed, we are not affiliated to Wargaming. This means that our posts aren't systematically sent to WG for reporting.
</p>

<p class="text-body">
While CS does occasionally receive some of our posts through tickets,
insight from Wargaming shows that this is only a minuscule percentage of the posts currently on our platform.
</p>
</div>

<div>
<h4>Why should I submit a ticket to Customer Support ?</h4>

<p class="text-body">
WOWS Karma has no power whatsoever on negative events, and <b>our staff is unable to take action on players</b>.
The only party able to effectively take action on players is Wargaming's Customer Support.
</p>

<p class="text-body">
If you encounter a player that is acting negatively, submitting a ticket to CS is the best way to get them sanctioned.
This is why, <strong>if you feel that the player you're posting on has broken the rules, we <u>strongly</u> encourage you to submit a ticket right away</strong>.
</p>
</div>

<div>
<h4>How do I submit a ticket to Customer Support ?</h4>
<p class="text-body">
To submit a ticket to WG's {{ region() }} Customer Support, you can follow these links depending on your issue :
</p>

<ul>
<li><a [href]="csLinks()[0]" rel="external" target="_blank">Report : Gameplay Violation / Collusions</a></li>
<li><a [href]="csLinks()[1]" rel="external" target="_blank">Report : Chat Issues / Inappropriate Behaviour</a></li>
</ul>

<p>
Once reported, then come back here with your Ticket ID.
This will help us, Wargaming, and others know that the affected user has already been reported for a given issue.
</p>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ChangeDetectionStrategy, Component, computed, inject, Input } from '@angular/core';
import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap";
import { AuthService } from "../../../services/api/services/auth.service";
import { AppConfigService } from "../../../services/app-config.service";

@Component({
selector: 'app-cs-ticket-id',
standalone: true,
imports: [],
templateUrl: './cs-ticket-id-help.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CsTicketIdHelpComponent {
@Input() modal!: NgbModalRef;

appConfig = inject(AppConfigService);

region = computed(() => this.appConfig.currentRegion);

csLinks = computed<[string, string]>(() => {
/*
* In order:
* - Gameplay / Collusions
* - Chat Issues
*
* See: https://github.com/SakuraIsayeki/WOWS-Karma/issues/165
*/

if (this.region() === 'EU') {
return [
'https://eu.wargaming.net/support/en/products/wows/help/29948/29949/29955/29957/',
'https://eu.wargaming.net/support/en/products/wows/help/29948/29949/29951/29952/'
];
} else if (this.region() === 'NA') {
return [
'https://na.wargaming.net/support/en/products/wows/help/31336/31337/31338/31339/',
'https://na.wargaming.net/support/en/products/wows/help/31336/31337/31345/'
];
} else if (this.region() === 'SEA') {
return [
"https://asia.wargaming.net/support/en/products/wows/help/28687/28688/28689/",
"https://asia.wargaming.net/support/en/products/wows/help/28687/28688/28694/"
];
}

return ['', ''];
})

constructor() {
}

static OpenModal(modalService: NgbModal) {
const modalRef = modalService.open(CsTicketIdHelpComponent, { size: "lg" });
modalRef.componentInstance.modal = modalRef;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,41 @@ <h5 class="my-0">{{group.label}}</h5>
</dl>
</div>

<div class="col" id="replay-upload">
<h4 class="mb-2">Replay File</h4>
<div class="col">
<div id="replay-upload">
<h4 class="mb-2">Replay File</h4>

<input name="replay-file" type="file" class="form-control" accept=".wowsreplay"
[formControl]="form.controls.replayFile" [formControlExtensions]="replayErrors"
>
<input name="replay-file" type="file" class="form-control" accept=".wowsreplay"
[formControl]="form.controls.replayFile" [formControlExtensions]="replayErrors"
>

<div class="my-2" #replayErrors>
<form-errors [control]="form.controls.replayFile"></form-errors>
<div class="my-2" #replayErrors>
<form-errors [control]="form.controls.replayFile"></form-errors>
</div>
</div>

<!-- If the second/third group of flairs is set to false, display the CS ticket field -->
@if (flairGroups.length > 1 && (flairGroups[1].control.value === false || flairGroups[2].control.value === false)) {
<div class="form-group my-3">
<div class="d-flex justify-content-between mb-1">
<span class="d-inline-flex gap-2 align-items-center">
<label for="ticket-id">CS Ticket ID</label>
<small class="text-muted"><i>(optional)</i></small>
</span>

<!-- Help button w/ popup -->
<button type="button" class="btn btn-sm text-info d-inline-flex gap-1" (click)="openCsTicketHelp()">
<span>What is this</span>
<i class="bi bi-question-circle"></i>
</button>
</div>

<input type="text" id="ticket-id" class="form-control" name="ticketId" placeholder="CS Ticket ID (optional)"
[formControl]="form.controls.supportTicketStatus.controls.ticketId">
</div>
}
</div>
</div>


</div>

<div class="modal-footer d-flex flex-column flex-md-row justify-content-between align-content-center">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, HostListener, Input } from "@angular/core";
import { ChangeDetectionStrategy, Component, HostListener, inject, Input } from "@angular/core";
import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap";
import { AccountClanListingDto } from "src/app/services/api/models/account-clan-listing-dto";
Expand All @@ -9,6 +9,7 @@ import { PostService } from "src/app/services/api/services/post.service";
import { markTouchedDirtyAndValidate, TypedFormControls, TypedFormGroup } from "src/app/services/helpers";
import { parseFlairsEnum, toEnum } from "src/app/services/metricsHelpers";
import * as ReplayValidators from "../../validation/replay-validators";
import { CsTicketIdHelpComponent } from "../cs-ticket-id-help/cs-ticket-id-help.component";

@Component({
selector: "post-editor",
Expand All @@ -19,6 +20,8 @@ export class PostEditorComponent {
@Input() post!: PlayerPostEditorDto;
@Input() modal!: NgbModalRef;

modalService = inject(NgbModal);

form = new FormBuilder().nonNullable.group({
id: "",
title: ["", [Validators.required, Validators.minLength(5), Validators.maxLength(60)]],
Expand All @@ -30,7 +33,11 @@ export class PostEditorComponent {
}),
replayFile: [null as File | null, [ReplayValidators.requireReplay]],
guidelinesAccepted: [false, Validators.requiredTrue],
modReason: ""
modReason: "",
supportTicketStatus: new FormBuilder().group({
hasTicket: [false],
ticketId: [null as number | null, Validators.maxLength(9)],
})
})

protected readonly flairsOptions = [
Expand Down Expand Up @@ -130,6 +137,10 @@ export class PostEditorComponent {

console.debug("Submitting post", this.post);
}

openCsTicketHelp() {
CsTicketIdHelpComponent.OpenModal(this.modalService);
}
}

export class PlayerPostEditorDto implements PlayerPostDto {
Expand All @@ -149,6 +160,7 @@ export class PlayerPostEditorDto implements PlayerPostDto {
replayFile: File | null = null;
guidelinesAccepted: boolean = false;

supportTicketStatus: { hasTicket: boolean, ticketId: number | null } = { hasTicket: false, ticketId: null };

static fromDto(dto: PlayerPostDto): PlayerPostEditorDto {
let p = dto as PlayerPostEditorDto;
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { AppConfigService } from "../../../services/app-config.service";
@Component({
selector: "modal-seed-token-change",
templateUrl: "./seed-token-change.component.html",
styleUrls: ["./seed-token-change.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SeedTokenChangeComponent {
Expand Down
128 changes: 67 additions & 61 deletions wowskarma.app/src/app/shared/post/post.component.html
Original file line number Diff line number Diff line change
@@ -1,78 +1,84 @@
<div *ngIf="post() as p" class="m-2 vstack" [id]="p.id">
<p *ngIf="postDisplayType() === 'neutral'" class="text-body">
<player-namelink [player]="p.author" [displayClan]="true" />
&gt;
<player-namelink [player]="p.player" [displayClan]="true" />
</p>
<p *ngIf="postDisplayType() === 'neutral'" class="text-body">
<player-namelink [player]="p.author" [displayClan]="true" />
&gt;
<player-namelink [player]="p.player" [displayClan]="true" />
</p>

<div class="card post-card border-{{p.flairs! | postBorderColor}}">
<div class="card-header">
<h5 class="my-2">{{ p.title }}</h5>
</div>

<div class="card-body">
<markdown class="card-text markdown" [data]="p.content" />
</div>
<div class="card post-card border-{{p.flairs! | postBorderColor}}">
<div class="card-header">
<h5 class="my-2">{{ p.title }}</h5>
</div>

<div class="card-body py-0 my-1" style="line-height: normal;">
<div class="row justify-content-between align-items-end">
<div class="col-auto">
<flairs-markup [flairsEnum]="p.flairs" />
</div>
<div class="card-body">
<markdown class="card-text markdown" [data]="p.content" />
</div>

<div class="col-auto px-1">
<i *ngIf="p.readOnly" class="bi bi-asterisk text-warning lead mx-1"></i>
<i *ngIf="p.modLocked" class="bi bi-x-circle text-danger lead mx-1"></i>
<div class="card-body py-0 my-1" style="line-height: normal;">
<div class="row justify-content-between align-items-end">
<div class="col-auto">
<flairs-markup [flairsEnum]="p.flairs" />
</div>

@switch (p.replayState) {
@case (2) {
<a class="text-body" [routerLink]="['/posts', 'view', p.id]">
<i class="bi bi-camera-video text-success lead mx-1"></i>
</a>
}
<div class="col-auto px-1">
<i *ngIf="p.readOnly" class="bi bi-asterisk text-warning lead mx-1"></i>
<i *ngIf="p.modLocked" class="bi bi-x-circle text-danger lead mx-1"></i>

@case (1) {
<a class="text-body" [routerLink]="['/posts', 'view', p.id]" title="Replay minimap is being processed.">
<i class="bi bi-camera-video text-warning lead mx-1"></i>
</a>
}
@switch (p.replayState) {
@case (2) {
<a class="text-body" [routerLink]="['/posts', 'view', p.id]">
<i class="bi bi-camera-video text-success lead mx-1"></i>
</a>
}
@case (1) {
<a class="text-body" [routerLink]="['/posts', 'view', p.id]" title="Replay minimap is being processed.">
<i class="bi bi-camera-video text-warning lead mx-1"></i>
</a>
}
@case (0) {
<i class="bi bi-camera-video-off text-danger lead mx-1"></i>
}
}

@case (0) {
<i class="bi bi-camera-video-off text-danger lead mx-1"></i>
}
}
@if (p.supportTicketStatus?.hasTicket) {
@if (p.supportTicketStatus?.ticketId) {
<i class="bi bi-flag text-success lead mx-1" ngbTooltip="CS Ticket ID : {{ p.supportTicketStatus!.ticketId }}"></i>
} @else {
<i class="bi bi-flag text-success lead mx-1" ngbTooltip="Reported to CS"></i>
}
}

<a class="text-body" [routerLink]="['/posts', 'view', p.id]">
<i class="bi bi-link-45deg lead"></i>
</a>
</div>
</div>
<a class="text-body" [routerLink]="['/posts', 'view', p.id]">
<i class="bi bi-link-45deg lead"></i>
</a>
</div>
</div>
</div>

<div class="card-footer">
<p class="blockquote-footer my-1">
<ng-container *ngIf="postDisplayType() === 'received'">
From
<player-namelink [player]="p.author" [displayClan]="true" />
</ng-container>
<div class="card-footer">
<p class="blockquote-footer my-1 d-inline-flex gap-1">
@if (postDisplayType() === 'received') {
<span>From <player-namelink [player]="p.author" [displayClan]="true" /></span>
}

<ng-container *ngIf="postDisplayType() === 'sent'">
To
<player-namelink [player]="p.player" [displayClan]="true" />
</ng-container>
@if (postDisplayType() === 'sent') {
<span>To <player-namelink [player]="p.player" [displayClan]="true" /></span>
}

{{ p.createdAt | date:'medium' }}
</p>
<span>{{ p.createdAt | date:'medium' }}</span>
</p>

<div class="d-flex flex-row my-1 gap-2">
<button *ngIf="canEdit" class="btn btn-warning btn-sm" (click)="openEditor()">Edit</button>
<button *ngIf="canDelete" class="btn btn-danger btn-sm" (click)="openDeleteModal()">Delete</button>
<div class="d-flex flex-row my-1 gap-2">
<div class="d-flex flex-row gap-1">
<button *ngIf="canEdit" class="btn btn-warning btn-sm" (click)="openEditor()">Edit</button>
<button *ngIf="canDelete" class="btn btn-danger btn-sm" (click)="openDeleteModal()">Delete</button>
</div>

<div class="d-flex flex-row gap-1" *ngIf="this.authService.isInRole('mod')">
<button class="btn btn-outline-warning btn-sm" (click)="openModEditor()">Mod Edit</button>
<button class="btn btn-outline-danger btn-sm" (click)="openModDeleteModal()">Mod Delete</button>
</div>
</div>
<div class="d-flex flex-row gap-1" *ngIf="this.authService.isInRole('mod')">
<button class="btn btn-outline-warning btn-sm" (click)="openModEditor()">Mod Edit</button>
<button class="btn btn-outline-danger btn-sm" (click)="openModDeleteModal()">Mod Delete</button>
</div>
</div>
</div>
</div>
</div>

0 comments on commit 2d228ab

Please sign in to comment.