diff --git a/src/Ombi.Core/Engine/V2/IssuesEngine.cs b/src/Ombi.Core/Engine/V2/IssuesEngine.cs new file mode 100644 index 000000000..830dd4295 --- /dev/null +++ b/src/Ombi.Core/Engine/V2/IssuesEngine.cs @@ -0,0 +1,81 @@ +using Microsoft.EntityFrameworkCore; +using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Ombi.Core.Engine.V2 +{ + + public interface IIssuesEngine + { + Task> GetIssues(int position, int take, IssueStatus status, CancellationToken token); + Task GetIssuesByProviderId(string providerId, CancellationToken token); + } + + public class IssuesEngine : IIssuesEngine + { + private readonly IRepository _categories; + private readonly IRepository _issues; + private readonly IRepository _comments; + + public IssuesEngine(IRepository categories, + IRepository issues, + IRepository comments) + { + _categories = categories; + _issues = issues; + _comments = comments; + } + + public async Task> GetIssues(int position, int take, IssueStatus status, CancellationToken token) + { + var issues = await _issues.GetAll().Where(x => x.Status == status && x.ProviderId != null).Skip(position).Take(take).OrderBy(x => x.Title).ToListAsync(token); + var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }); + + var model = new List(); + + foreach(var group in grouped) + { + model.Add(new IssuesSummaryModel + { + Count = group.Issues.Count(), + Title = group.Title, + ProviderId = group.Issues.FirstOrDefault()?.ProviderId + }); + } + + return model; + } + + public async Task GetIssuesByProviderId(string providerId, CancellationToken token) + { + var issues = await _issues.GetAll().Include(x => x.Comments).ThenInclude(x => x.User).Include(x => x.UserReported).Include(x => x.IssueCategory).Where(x => x.ProviderId == providerId).ToListAsync(token); + var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }).FirstOrDefault(); + + if (grouped == null) + { + return null; + } + + return new IssuesSummaryModel + { + Count = grouped.Issues.Count(), + Title = grouped.Title, + ProviderId = grouped.Issues.FirstOrDefault()?.ProviderId, + Issues = grouped.Issues + }; + } + + } + + public class IssuesSummaryModel + { + public string Title { get; set; } + public int Count { get; set; } + public string ProviderId { get; set; } + public IEnumerable Issues { get; set; } + } +} diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index d5e577d84..ef9c9230c 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -114,6 +114,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void RegisterHttp(this IServiceCollection services) diff --git a/src/Ombi/ClientApp/src/app/app.module.ts b/src/Ombi/ClientApp/src/app/app.module.ts index cfaf783cf..4de2c9f9f 100644 --- a/src/Ombi/ClientApp/src/app/app.module.ts +++ b/src/Ombi/ClientApp/src/app/app.module.ts @@ -145,6 +145,7 @@ export function JwtTokenGetter() { MatCheckboxModule, MatProgressSpinnerModule, MDBBootstrapModule.forRoot(), + // NbThemeModule.forRoot({ name: 'dark'}), JwtModule.forRoot({ config: { tokenGetter: JwtTokenGetter, diff --git a/src/Ombi/ClientApp/src/app/auth/auth.service.ts b/src/Ombi/ClientApp/src/app/auth/auth.service.ts index 3eace43a4..afc0a2491 100644 --- a/src/Ombi/ClientApp/src/app/auth/auth.service.ts +++ b/src/Ombi/ClientApp/src/app/auth/auth.service.ts @@ -74,6 +74,10 @@ export class AuthService extends ServiceHelpers { return false; } + public isAdmin() { + return this.hasRole("Admin") || this.hasRole("PowerUser"); + } + public logout() { this.store.remove("id_token"); } diff --git a/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts b/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts index dce9882ec..10de2a596 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts @@ -62,3 +62,10 @@ export interface IUpdateStatus { issueId: number; status: IssueStatus; } + +export interface IIssuesSummary { + title: string; + count: number; + providerId: string; + issues: IIssues[]; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html new file mode 100644 index 000000000..c4906baac --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html @@ -0,0 +1,27 @@ + + + {{issue.subject}} + {{issue.userReported?.userName}} on {{issue.createdDate | date:short}} + + +

+ {{issue.description}} +

+
+ + +
here is ignored
+ + + + + + + +
+
+ diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss new file mode 100644 index 000000000..c244fdd09 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss @@ -0,0 +1,9 @@ +@import "~styles/variables.scss"; + +::ng-deep .issue-card { + border: 3px solid $ombi-background-primary-accent; +} + +.top-spacing { + margin-top:2%; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts new file mode 100644 index 000000000..4f7ddc612 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts @@ -0,0 +1,55 @@ +import { Component, Input } from "@angular/core"; +import { MatDialog } from '@angular/material/dialog'; +import { TranslateService } from "@ngx-translate/core"; +import { IIssues, IIssueSettings, IssueStatus } from "../../../interfaces"; +import { IssuesService, NotificationService } from "../../../services"; +import { IssueChatComponent } from "../issue-chat/issue-chat.component"; + +@Component({ + selector: "issues-details-group", + templateUrl: "details-group.component.html", + styleUrls: ["details-group.component.scss"], +}) +export class DetailsGroupComponent { + + @Input() public issue: IIssues; + @Input() public isAdmin: boolean; + @Input() public settings: IIssueSettings; + + public deleted: boolean; + public IssueStatus = IssueStatus; + public get hasRequest(): boolean { + if (this.issue.requestId) { + return true; + } + return false; + } + + constructor( + private translateService: TranslateService, private issuesService: IssuesService, + private notificationService: NotificationService, private dialog: MatDialog) { } + + public async delete(issue: IIssues) { + await this.issuesService.deleteIssue(issue.id); + this.notificationService.success(this.translateService.instant("Issues.DeletedIssue")); + this.deleted = true; + } + + public openChat(issue: IIssues) { + this.dialog.open(IssueChatComponent, { width: "100vh", data: { issueId: issue.id }, panelClass: 'modal-panel' }) + } + + public resolve(issue: IIssues) { + this.issuesService.updateStatus({issueId: issue.id, status: IssueStatus.Resolved}).subscribe(() => { + this.notificationService.success(this.translateService.instant("Issues.MarkedAsResolved")); + issue.status = IssueStatus.Resolved; + }); + } + + public inProgress(issue: IIssues) { + this.issuesService.updateStatus({issueId: issue.id, status: IssueStatus.InProgress}).subscribe(() => { + this.notificationService.success(this.translateService.instant("Issues.MarkedAsInProgress")); + issue.status = IssueStatus.InProgress; + }); + } +} diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html new file mode 100644 index 000000000..bc0e8a794 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html @@ -0,0 +1,20 @@ +
+ +

Issues for {{details.title}}

+
+ {{'Issues.Requested' | translate}} + + + +
+ +
+
+
+ +
+
+
+
+
+
diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.scss b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.scss new file mode 100644 index 000000000..d6dcd67de --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.scss @@ -0,0 +1,9 @@ +@import "~styles/variables.scss"; + +::ng-deep .mat-card { + background: $ombi-background-primary-accent; +} + +.top-spacing { + margin-top:2%; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts new file mode 100644 index 000000000..4003acd40 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts @@ -0,0 +1,93 @@ +import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core"; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ActivatedRoute, ActivatedRouteSnapshot, Router } from "@angular/router"; +import { TranslateService } from "@ngx-translate/core"; +import { AuthService } from "../../../auth/auth.service"; +import { IIssues, IIssueSettings, IIssuesSummary, IssueStatus, RequestType } from "../../../interfaces"; +import { IssuesService, NotificationService, SettingsService } from "../../../services"; +import { IssuesV2Service } from "../../../services/issuesv2.service"; +import { IssueChatComponent } from "../issue-chat/issue-chat.component"; + + +export interface IssuesDetailsGroupData { + issues: IIssues[]; + title: string; +} + +@Component({ + selector: "issues-details", + templateUrl: "details.component.html", + styleUrls: ["details.component.scss"], + encapsulation: ViewEncapsulation.None +}) +export class IssuesDetailsComponent implements OnInit { + + public details: IIssuesSummary; + public isAdmin: boolean; + public IssueStatus = IssueStatus; + public settings: IIssueSettings; + public get hasRequest(): boolean { + return this.details.issues.some(x => x.requestId); + } + + private providerId: string; + + constructor(private authService: AuthService, private settingsService: SettingsService, + private issueServiceV2: IssuesV2Service, private route: ActivatedRoute, private router: Router, + private issuesService: IssuesService, private translateService: TranslateService, private notificationService: NotificationService, + private dialog: MatDialog) { + this.route.params.subscribe(async (params: any) => { + if (typeof params.providerId === 'string' || params.providerId instanceof String) { + this.providerId = params.providerId; + } + }); + } + + public ngOnInit() { + this.isAdmin = this.authService.hasRole("Admin") || this.authService.hasRole("PowerUser"); + this.settingsService.getIssueSettings().subscribe(x => this.settings = x); + this.issueServiceV2.getIssuesByProviderId(this.providerId).subscribe(x => this.details = x); + } + + public resolve(issue: IIssues) { + this.issuesService.updateStatus({issueId: issue.id, status: IssueStatus.Resolved}).subscribe(x => { + this.notificationService.success(this.translateService.instant("Issues.MarkedAsResolved")); + issue.status = IssueStatus.Resolved; + }); + } + + public inProgress(issue: IIssues) { + this.issuesService.updateStatus({issueId: issue.id, status: IssueStatus.InProgress}).subscribe(x => { + this.notificationService.success(this.translateService.instant("Issues.MarkedAsInProgress")); + issue.status = IssueStatus.InProgress; + }); + } + + public async delete(issue: IIssues) { + await this.issuesService.deleteIssue(issue.id); + this.notificationService.success(this.translateService.instant("Issues.DeletedIssue")); + this.details.issues = this.details.issues.filter((el) => { return el.id !== issue.id; }); + } + + public openChat(issue: IIssues) { + this.dialog.open(IssueChatComponent, { width: "100vh", data: { issueId: issue.id }, panelClass: 'modal-panel' }) + } + + public navToMedia() { + const firstIssue = this.details.issues[0]; + switch(firstIssue.requestType) { + case RequestType.movie: + this.router.navigate(['/details/movie/', firstIssue.providerId]); + return; + + case RequestType.album: + this.router.navigate(['/details/artist/', firstIssue.providerId]); + return; + + case RequestType.tvShow: + this.router.navigate(['/details/tv/', firstIssue.providerId]); + return; + } + } + +} diff --git a/src/Ombi/ClientApp/src/app/issues/components/index.ts b/src/Ombi/ClientApp/src/app/issues/components/index.ts index 93bb4907d..52e4ed03c 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/index.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/index.ts @@ -1,20 +1,19 @@ -import { AuthGuard } from "../../auth/auth.guard"; -import { IssuesListComponent } from "./issues-list/issues-list.component"; -import { Routes } from "@angular/router"; +import { IssuesV2Service } from "../../services/issuesv2.service"; +import { IdentityService, SearchService } from "../../services"; +import { IssuesDetailsComponent } from "./details/details.component"; +import { IssueChatComponent } from "./issue-chat/issue-chat.component"; +import { ChatBoxComponent } from "../../shared/chat-box/chat-box.component"; export const components: any[] = [ - IssuesListComponent, -]; - - -export const entryComponents: any[] = [ + IssuesDetailsComponent, + IssueChatComponent, + ChatBoxComponent, ]; export const providers: any[] = [ -]; - -export const routes: Routes = [ - { path: "", component: IssuesListComponent, canActivate: [AuthGuard] }, + IssuesV2Service, + IdentityService, + SearchService, ]; \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.html b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.html new file mode 100644 index 000000000..fe6c13ce7 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.html @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.scss b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.scss new file mode 100644 index 000000000..d6dcd67de --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.scss @@ -0,0 +1,9 @@ +@import "~styles/variables.scss"; + +::ng-deep .mat-card { + background: $ombi-background-primary-accent; +} + +.top-spacing { + margin-top:2%; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts new file mode 100644 index 000000000..023fdbf12 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts @@ -0,0 +1,87 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { AuthService } from "../../../auth/auth.service"; +import { ILocalUser } from "../../../auth/IUserLogin"; +import { IIssuesChat, IIssueSettings, IssueStatus } from "../../../interfaces"; +import { IssuesService, SettingsService } from "../../../services"; +import { ChatMessages, ChatType } from "../../../shared/chat-box/chat-box.component"; + + +export interface ChatData { + issueId: number; + title: string; + } + +@Component({ + selector: "issue-chat", + templateUrl: "issue-chat.component.html", + styleUrls: ["issue-chat.component.scss"], +}) +export class IssueChatComponent implements OnInit { + + public isAdmin: boolean; + public comments: IIssuesChat[] = []; + public IssueStatus = IssueStatus; + public settings: IIssueSettings; + public messages: ChatMessages[] = []; + + public loaded: boolean; + + private user: ILocalUser; + + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ChatData, + private authService: AuthService, private settingsService: SettingsService, + private issueService: IssuesService) { } + + public ngOnInit() { + this.isAdmin = this.authService.isAdmin(); + this.user = this.authService.claims(); + this.settingsService.getIssueSettings().subscribe(x => this.settings = x); + this.issueService.getComments(this.data.issueId).subscribe(x => { + this.comments = x; + this.mapMessages(); + this.loaded = true; + }); + } + + + public deleteComment(commentId: number) { + + } + + public addComment(comment: string) { + this.issueService.addComment({ + comment: comment, + issueId: this.data.issueId + }).subscribe(comment => { + this.messages.push({ + chatType: ChatType.Sender, + date: comment.date, + id: -1, + message: comment.comment, + username: comment.user.userName + }); + }); + + } + + public close() { + this.dialogRef.close(); + } + + private mapMessages() { + this.comments.forEach((m: IIssuesChat) => { + this.messages.push({ + chatType: m.username === this.user.name ? ChatType.Sender : ChatType.Reciever, + date: m.date, + id: m.id, + message: m.comment, + username: m.username + }); + }); + } + + +} diff --git a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.html b/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.html deleted file mode 100644 index 61f154193..000000000 --- a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.html +++ /dev/null @@ -1,21 +0,0 @@ - \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.ts b/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.ts deleted file mode 100644 index 82883a4f2..000000000 --- a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Component, OnInit } from "@angular/core"; - -import { IssuesService } from "../../../services"; - -import { IIssueCount, IIssues, IPagenator, IssueStatus } from "../../../interfaces"; -import { COLUMNS } from "./issues-list.constants"; - -@Component({ - selector: "issues-list", - templateUrl: "issues-list.component.html", -}) -export class IssuesListComponent implements OnInit { - - public columnsToDisplay = COLUMNS - - public pendingIssues: IIssues[]; - public inProgressIssues: IIssues[]; - public resolvedIssues: IIssues[]; - - public count: IIssueCount; - - private takeAmount = 10; - private pendingSkip = 0; - private inProgressSkip = 0; - private resolvedSkip = 0; - - constructor(private issueService: IssuesService) { } - - public ngOnInit() { - this.getPending(); - this.getInProg(); - this.getResolved(); - this.issueService.getIssuesCount().subscribe(x => this.count = x); - } - - public changePagePending(event: IPagenator) { - this.pendingSkip = event.first; - this.getPending(); - } - - public changePageInProg(event: IPagenator) { - this.inProgressSkip = event.first; - this.getInProg(); - } - - public changePageResolved(event: IPagenator) { - this.resolvedSkip = event.first; - this.getResolved(); - } - - private getPending() { - this.issueService.getIssuesPage(this.takeAmount, this.pendingSkip, IssueStatus.Pending).subscribe(x => { - this.pendingIssues = x; - }); - } - - private getInProg() { - this.issueService.getIssuesPage(this.takeAmount, this.inProgressSkip, IssueStatus.InProgress).subscribe(x => { - this.inProgressIssues = x; - }); - } - - private getResolved() { - this.issueService.getIssuesPage(this.takeAmount, this.resolvedSkip, IssueStatus.Resolved).subscribe(x => { - this.resolvedIssues = x; - }); - } -} diff --git a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.constants.ts b/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.constants.ts deleted file mode 100644 index f4a00e24b..000000000 --- a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const COLUMNS = [ - "title" -] \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html b/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html index c2de76c2b..b5fc59304 100644 --- a/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html @@ -84,7 +84,7 @@
- +
diff --git a/src/Ombi/ClientApp/src/app/issues/issues.component.html b/src/Ombi/ClientApp/src/app/issues/issues.component.html index ff8440569..72734ed85 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issues.component.html @@ -1,25 +1,28 @@
- - - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
-
+
+ + + + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/issues.component.ts b/src/Ombi/ClientApp/src/app/issues/issues.component.ts index b8482f1ff..5dc02845e 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issues.component.ts @@ -2,9 +2,10 @@ import { Component, OnInit } from "@angular/core"; import { IssuesService } from "../services"; -import { IIssueCount, IIssues, IPagenator, IssueStatus } from "../interfaces"; +import { IIssueCount, IIssues, IIssuesSummary, IPagenator, IssueStatus } from "../interfaces"; import { PageEvent } from '@angular/material/paginator'; +import { IssuesV2Service } from "../services/issuesv2.service"; @Component({ templateUrl: "issues.component.html", @@ -12,9 +13,9 @@ import { PageEvent } from '@angular/material/paginator'; }) export class IssuesComponent implements OnInit { - public pendingIssues: IIssues[]; - public inProgressIssues: IIssues[]; - public resolvedIssues: IIssues[]; + public pendingIssues: IIssuesSummary[]; + public inProgressIssues: IIssuesSummary[]; + public resolvedIssues: IIssuesSummary[]; public count: IIssueCount; @@ -23,7 +24,7 @@ export class IssuesComponent implements OnInit { private inProgressSkip = 0; private resolvedSkip = 0; - constructor(private issueService: IssuesService) { } + constructor(private issuev2Service: IssuesV2Service, private issueService: IssuesService) { } public ngOnInit() { this.getPending(); @@ -48,19 +49,19 @@ export class IssuesComponent implements OnInit { } private getPending() { - this.issueService.getIssuesPage(this.takeAmount, this.pendingSkip, IssueStatus.Pending).subscribe(x => { + this.issuev2Service.getIssues(this.pendingSkip, this.takeAmount, IssueStatus.Pending).subscribe(x => { this.pendingIssues = x; }); } private getInProg() { - this.issueService.getIssuesPage(this.takeAmount, this.inProgressSkip, IssueStatus.InProgress).subscribe(x => { + this.issuev2Service.getIssues(this.inProgressSkip, this.takeAmount, IssueStatus.InProgress).subscribe(x => { this.inProgressIssues = x; }); } private getResolved() { - this.issueService.getIssuesPage(this.takeAmount, this.resolvedSkip, IssueStatus.Resolved).subscribe(x => { + this.issuev2Service.getIssues(this.resolvedSkip, this.takeAmount, IssueStatus.Resolved).subscribe(x => { this.resolvedIssues = x; }); } diff --git a/src/Ombi/ClientApp/src/app/issues/issues.module.ts b/src/Ombi/ClientApp/src/app/issues/issues.module.ts index 2607d336a..4bf232f25 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.module.ts +++ b/src/Ombi/ClientApp/src/app/issues/issues.module.ts @@ -1,17 +1,17 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +// import { NbChatModule, NbThemeModule } from '@nebular/theme'; import { OrderModule } from "ngx-order-pipe"; -import { IdentityService, SearchService } from "../services"; - import { AuthGuard } from "../auth/auth.guard"; -import { SharedModule as OmbiShared } from "../shared/shared.module"; +import { SharedModule } from "../shared/shared.module"; import { IssueDetailsComponent } from "./issueDetails.component"; import { IssuesComponent } from "./issues.component"; import { IssuesTableComponent } from "./issuestable.component"; +import { IssuesDetailsComponent } from "./components/details/details.component"; import { PipeModule } from "../pipes/pipe.module"; @@ -19,7 +19,7 @@ import * as fromComponents from "./components"; const routes: Routes = [ { path: "", component: IssuesComponent, canActivate: [AuthGuard] }, - { path: ":id", component: IssueDetailsComponent, canActivate: [AuthGuard] }, + { path: ":providerId", component: IssuesDetailsComponent, canActivate: [AuthGuard] }, ]; @NgModule({ @@ -27,7 +27,8 @@ const routes: Routes = [ RouterModule.forChild(routes), OrderModule, PipeModule, - OmbiShared, + SharedModule, + // NbChatModule, ], declarations: [ IssuesComponent, @@ -39,8 +40,7 @@ const routes: Routes = [ RouterModule, ], providers: [ - IdentityService, - SearchService, + ...fromComponents.providers ], }) diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.html b/src/Ombi/ClientApp/src/app/issues/issuestable.component.html index e266a718a..8e22d3e51 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.html @@ -6,32 +6,15 @@ {{element.title}} - - {{ 'Issues.Category' | translate}} - {{element.issueCategory.value}} - - - - {{ 'Issues.Subject' | translate}} - {{element.subject}} - - - - {{ 'Issues.Status' | translate}} - {{IssueStatus[element.status] | humanize}} - - - - {{ 'Issues.ReportedBy' | translate}} - {{element.userReported.userAlias}} + + {{ 'Issues.Count' | translate}} + {{element.count}} - - - + diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts index ff584a35e..0fd81a534 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; -import { IIssues, IPagenator, IssueStatus } from "../interfaces"; +import { IIssuesSummary, IPagenator, IssueStatus } from "../interfaces"; @Component({ selector: "issues-table", @@ -8,12 +9,14 @@ import { IIssues, IPagenator, IssueStatus } from "../interfaces"; }) export class IssuesTableComponent { - @Input() public issues: IIssues[]; + constructor(public dialog: MatDialog) { } + + @Input() public issues: IIssuesSummary[]; @Input() public totalRecords: number; @Output() public changePage = new EventEmitter(); - public displayedColumns = ["title", "category", "subject", "status", "reportedBy", "actions"] + public displayedColumns = ["title", "count", "actions"] public IssueStatus = IssueStatus; public resultsLength: number; public gridCount: string = "15"; @@ -48,5 +51,4 @@ export class IssuesTableComponent { public paginate(event: IPagenator) { this.changePage.emit(event); } - } diff --git a/src/Ombi/ClientApp/src/app/media-details/components/index.ts b/src/Ombi/ClientApp/src/app/media-details/components/index.ts index 240fba28a..e8cb89d4a 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/index.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/index.ts @@ -20,6 +20,7 @@ import { IssuesPanelComponent } from "./shared/issues-panel/issues-panel.compone import { TvAdvancedOptionsComponent } from "./tv/panels/tv-advanced-options/tv-advanced-options.component"; import { RequestBehalfComponent } from "./shared/request-behalf/request-behalf.component"; import { TvRequestGridComponent } from "./tv/panels/tv-request-grid/tv-request-grid.component"; +import { DetailsGroupComponent } from "../../issues/components/details-group/details-group.component"; export const components: any[] = [ MovieDetailsComponent, diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts index a9bdf073a..97cc75802 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts @@ -115,9 +115,13 @@ export class MovieDetailsComponent { } public async issue() { + let provider = this.movie.id.toString(); + if (this.movie.imdbId) { + provider = this.movie.imdbId; + } const dialogRef = this.dialog.open(NewIssueComponent, { width: '500px', - data: { requestId: this.movieRequest ? this.movieRequest.id : null, requestType: RequestType.movie, providerId: this.movie.imdbId ? this.movie.imdbId : this.movie.id, title: this.movie.title } + data: { requestId: this.movieRequest ? this.movieRequest.id : null, requestType: RequestType.movie, providerId: provider, title: this.movie.title } }); } diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.html index c4c05dff9..d71ca8fcd 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.html @@ -13,6 +13,11 @@ + +
+ +
+
diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.ts index dda7bcb89..206be2141 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.ts @@ -6,10 +6,11 @@ import { TranslateService } from "@ngx-translate/core"; @Component({ selector: "issues-panel", templateUrl: "./issues-panel.component.html", - styleUrls: ["./issues-panel.component.scss"] + styleUrls: ["./issues-panel.component.scss"], + }) export class IssuesPanelComponent implements OnInit { - + @Input() public providerId: string; @Input() public isAdmin: boolean; @@ -22,7 +23,6 @@ export class IssuesPanelComponent implements OnInit { constructor(private issuesService: IssuesService, private notificationService: NotificationService, private translateService: TranslateService, private settingsService: SettingsService) { - } public async ngOnInit() { @@ -54,7 +54,7 @@ export class IssuesPanelComponent implements OnInit { this.issuesCount = this.issues.length; this.calculateOutstanding(); } - + private calculateOutstanding() { this.isOutstanding = this.issues.some((i) => { return i.status !== IssueStatus.Resolved; diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/new-issue/new-issue.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/new-issue/new-issue.component.ts index c42c14fe2..94972c70d 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/shared/new-issue/new-issue.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/new-issue/new-issue.component.ts @@ -10,7 +10,7 @@ import { TranslateService } from "@ngx-translate/core"; templateUrl: "./new-issue.component.html", }) export class NewIssueComponent implements OnInit { - + public issue: IIssues; public issueCategories: IIssueCategory[]; @@ -40,9 +40,9 @@ export class NewIssueComponent implements OnInit { public async ngOnInit(): Promise { this.issueCategories = await this.issueService.getCategories().toPromise(); - } + } - public async createIssue() { + public async createIssue() { const result = await this.issueService.createIssue(this.issue).toPromise(); if(result) { this.messageService.send(this.translate.instant("Issues.IssueDialog.IssueCreated")); diff --git a/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts b/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts new file mode 100644 index 000000000..d792702a0 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts @@ -0,0 +1,23 @@ +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; + +import { HttpClient } from "@angular/common/http"; +import { Observable } from "rxjs"; + +import { IIssueCategory, IIssueComments, IIssueCount, IIssues, IIssuesChat, IIssuesSummary, INewIssueComments, IssueStatus, IUpdateStatus } from "../interfaces"; +import { ServiceHelpers } from "./service.helpers"; + +@Injectable() +export class IssuesV2Service extends ServiceHelpers { + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v2/Issues/", href); + } + + public getIssues(position: number, take: number, status: IssueStatus): Observable { + return this.http.get(`${this.url}${position}/${take}/${status}`, {headers: this.headers}); + } + + public getIssuesByProviderId(providerId: string): Observable { + return this.http.get(`${this.url}details/${providerId}`, {headers: this.headers}); + } +} diff --git a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html new file mode 100644 index 000000000..aa19bf1db --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html @@ -0,0 +1,23 @@ +
+
+
+

Users

+
+

{{user}}

+
+
+
+
+
+

{{m.username}}

+
+

{{m.message}}

+

{{m.date | amLocal | amDateFormat: 'l LT'}}

+
+
+
+
+ + +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss new file mode 100644 index 000000000..70f83b85f --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss @@ -0,0 +1,330 @@ +// Variables +$primary: rgba(23, 190, 187, 1); +$secondary: rgba(240, 166, 202, 1); + +$active: rgba(23, 190, 187, 0.8); +$busy: rgba(252, 100, 113, 0.8); +$away: rgba(255, 253, 130, 0.8); + +// Triangle Mixin +@mixin triangle($color, $size, $direction) { + width: 0; + height: 0; + @if $direction == "up" { + border-right: ($size + px) solid transparent; + border-left: ($size + px) solid transparent; + border-bottom: ($size + px) solid $color; + } + @if $direction == "down" { + border-right: ($size + px) solid transparent; + border-left: ($size + px) solid transparent; + border-top: ($size + px) solid $color; + } + @if $direction == "right" { + border-top: ($size + px) solid transparent; + border-bottom: ($size + px) solid transparent; + border-left: ($size + px) solid $color; + } + @if $direction == "left" { + border-top: ($size + px) solid transparent; + border-bottom: ($size + px) solid transparent; + border-right: ($size + px) solid $color; + } +} + +* { + margin: 0; padding: 0; + box-sizing: border-box; + font-family: 'Nunito', sans-serif; +} + +html,body { + background: linear-gradient(120deg, $primary, $secondary); + overflow: hidden; +} + +.container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 70vh; + width: 100%; + h1 { + margin: 0.5em auto; + color: #FFF; + text-align: center; + } +} + +.chatbox { + background: rgba(255, 255, 255, 0.05); + width: 85%; + height: 100%; + border-radius: 0.2em; + position: relative; + box-shadow: 1px 1px 12px rgba(0, 0, 0, 0.1); + + .sender { + float: right; + &:after { + content: ''; + position: absolute; + margin: -1.5em -18.98em; + @include triangle(rgba(255, 255, 255, 0.2), 10, left); + } + } + .reciever { + float: left; + &:after { + content: ''; + position: absolute; + margin: -1.5em 2.65em; + @include triangle(rgba(255, 255, 255, 0.2), 10, right); + } + } + &__messages__user-message { + width: 450px; + } + &__messages__user-message--ind-message { + background: rgba(255, 255, 255, 0.2); + padding: 1em 0; + height: auto; + width: 65%; + border-radius: 5px; + margin: 2em 1em; + overflow: auto; + & > p.name { + color: #FFF; + font-size: 1em; + } + & > p.message { + color: #FFF; + font-size: 0.7em; + margin: 0 2.8em; + }& > p.timestamp { + color: #FFF; + font-size: 0.7em; + margin: 0 2.8em; + } + } + &__user-list { + background: rgba(255, 255, 255, 0.1); + width: 25%; + height: 100%; + float: right; + border-top-right-radius: 0.2em; + border-bottom-right-radius: 0.2em; + h1 { + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.9); + font-size: 0.9em; + padding: 1em; + margin: 0; + font-weight: 300; + text-align: center; + } + } + &__user { + width: 0.5em; + height: 0.5em; + border-radius: 100%; + margin: 1em 0.7em; + &--active { + @extend .chatbox__user; + background: $active; + } + &--busy { + @extend .chatbox__user; + background: $busy; + } + &--away { + @extend .chatbox__user; + background: $away; + } + } + p { + float: left; + text-align: left; + margin: -0.25em 2em; + font-size: 0.7em; + font-weight: 300; + color: #FFF; + width: 200px; + } + .form { + background: #222; + input { + background: rgba(255, 255, 255, 0.03); + position: absolute; + bottom: 0; + left: 0; + border: none; + width: 75%; + padding: 1.2em; + outline: none; + color: rgba(255, 255, 255, 0.9); + font-weight: 300; + } + .add-message { + background: rgba(255, 255, 255, 0.03); + position: absolute; + bottom: 1.5%; + right: 26%; + border: none; + outline: none; + color: rgba(255, 255, 255, 0.9); + font-weight: 300; + } + } +} + +// Placeholder Styling +::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.9); +} + +:-moz-placeholder { + color: rgba(255, 255, 255, 0.9); +} + +::-moz-placeholder { + color: rgba(255, 255, 255, 0.9); +} + +:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.9); +} + +// ::-webkit-scrollbar { +// width: 4px; +// } +// ::-webkit-scrollbar-thumb { +// background-color: #4c4c6a; +// border-radius: 2px; +// } +// .chatbox { +// width: 300px; +// height: 400px; +// max-height: 400px; +// display: flex; +// flex-direction: column; +// overflow: hidden; +// box-shadow: 0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28); +// } +// .chat-window { +// flex: auto; +// max-height: calc(100% - 60px); +// background: #2f323b; +// overflow: auto; +// } +// .chat-input { +// flex: 0 0 auto; +// height: 60px; +// background: #40434e; +// border-top: 1px solid #2671ff; +// box-shadow: 0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28); +// } +// .chat-input input { +// height: 59px; +// line-height: 60px; +// outline: 0 none; +// border: none; +// width: calc(100% - 60px); +// color: white; +// text-indent: 10px; +// font-size: 12pt; +// padding: 0; +// background: #40434e; +// } +// .chat-input button { +// float: right; +// outline: 0 none; +// border: none; +// background: rgba(255,255,255,.25); +// height: 40px; +// width: 40px; +// border-radius: 50%; +// padding: 2px 0 0 0; +// margin: 10px; +// transition: all 0.15s ease-in-out; +// } +// .chat-input input[good] + button { +// box-shadow: 0 0 2px rgba(0,0,0,.12),0 2px 4px rgba(0,0,0,.24); +// background: #2671ff; +// } +// .chat-input input[good] + button:hover { +// box-shadow: 0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); +// } +// .chat-input input[good] + button path { +// fill: white; +// } +// .msg-container { +// position: relative; +// display: inline-block; +// width: 100%; +// margin: 0 0 10px 0; +// padding: 0; +// } +// .msg-box { +// display: flex; +// background: #5b5e6c; +// padding: 10px 10px 0 10px; +// border-radius: 0 6px 6px 0; +// max-width: 80%; +// width: auto; +// float: left; +// box-shadow: 0 0 2px rgba(0,0,0,.12),0 2px 4px rgba(0,0,0,.24); +// } +// .user-img { +// display: inline-block; +// border-radius: 50%; +// height: 40px; +// width: 40px; +// background: #2671ff; +// margin: 0 10px 10px 0; +// } +// .flr { +// flex: 1 0 auto; +// display: flex; +// flex-direction: column; +// width: calc(100% - 50px); +// } +// .messages { +// flex: 1 0 auto; +// } +// .msg { +// display: inline-block; +// font-size: 11pt; +// line-height: 13pt; +// color: rgba(255,255,255,.7); +// margin: 0 0 4px 0; +// } +// .msg:first-of-type { +// margin-top: 8px; +// } +// .timestamp { +// color: rgba(0,0,0,.38); +// font-size: 8pt; +// margin-bottom: 10px; +// } +// .username { +// margin-right: 3px; +// } +// .posttime { +// margin-left: 3px; +// } +// .msg-self .msg-box { +// border-radius: 6px 0 0 6px; +// background: #2671ff; +// float: right; +// } +// .msg-self .user-img { +// margin: 0 0 10px 10px; +// } +// .msg-self .msg { +// text-align: right; +// } +// .msg-self .timestamp { +// text-align: right; +// } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.ts b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.ts new file mode 100644 index 000000000..697977563 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.ts @@ -0,0 +1,46 @@ +import { AfterContentChecked, AfterViewInit, Component, EventEmitter, Inject, Input, OnInit, Output } from "@angular/core"; + + +export interface ChatMessages { + id: number; + message: string; + date: Date; + username: string; + chatType: ChatType; + } + + export enum ChatType { + Sender, + Reciever + } + +@Component({ + selector: "ombi-chat-box", + templateUrl: "chat-box.component.html", + styleUrls: ["chat-box.component.scss"], +}) +export class ChatBoxComponent implements OnInit { + @Input() messages: ChatMessages[]; + @Output() onAddMessage: EventEmitter = new EventEmitter(); + @Output() onDeleteMessage: EventEmitter = new EventEmitter(); + + public currentMessage: string; + public userList: string[]; + public ChatType = ChatType; + + public ngOnInit(): void { + const allUsernames = this.messages.map(x => x.username); + this.userList = allUsernames.filter((v, i, a) => a.indexOf(v) === i); + } + + public deleteMessage(id: number) { + this.onDeleteMessage.emit(id); + } + + public addMessage() { + if (this.currentMessage) { + this.onAddMessage.emit(this.currentMessage); + this.currentMessage = ''; + } + } +} diff --git a/src/Ombi/ClientApp/src/app/shared/shared.module.ts b/src/Ombi/ClientApp/src/app/shared/shared.module.ts index ed93fa375..afe27bb7e 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -36,11 +36,13 @@ import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; import { MatSlideToggleModule } from "@angular/material/slide-toggle"; import { MatTabsModule } from "@angular/material/tabs"; import { EpisodeRequestComponent } from "./episode-request/episode-request.component"; +import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component"; @NgModule({ declarations: [ IssuesReportComponent, EpisodeRequestComponent, + DetailsGroupComponent, ], imports: [ SidebarModule, @@ -76,6 +78,7 @@ import { EpisodeRequestComponent } from "./episode-request/episode-request.compo ], entryComponents: [ EpisodeRequestComponent, + DetailsGroupComponent, ], exports: [ TranslateModule, @@ -86,6 +89,7 @@ import { EpisodeRequestComponent } from "./episode-request/episode-request.compo MatProgressSpinnerModule, IssuesReportComponent, EpisodeRequestComponent, + DetailsGroupComponent, TruncateModule, InputSwitchModule, MatTreeModule, diff --git a/src/Ombi/ClientApp/src/index.html b/src/Ombi/ClientApp/src/index.html index 9b4005ca1..4d077697d 100644 --- a/src/Ombi/ClientApp/src/index.html +++ b/src/Ombi/ClientApp/src/index.html @@ -14,6 +14,7 @@ + diff --git a/src/Ombi/Controllers/V1/IssuesController.cs b/src/Ombi/Controllers/V1/IssuesController.cs index dccceea3a..d75389c3a 100644 --- a/src/Ombi/Controllers/V1/IssuesController.cs +++ b/src/Ombi/Controllers/V1/IssuesController.cs @@ -142,7 +142,7 @@ namespace Ombi.Controllers.V1 var notificationModel = new NotificationOptions { RequestId = i.RequestId ?? 0, - DateTime = DateTime.Now, + DateTime = DateTime.UtcNow, NotificationType = NotificationType.Issue, RequestType = i.RequestType, Recipient = string.Empty, diff --git a/src/Ombi/Controllers/V2/IssuesController.cs b/src/Ombi/Controllers/V2/IssuesController.cs new file mode 100644 index 000000000..84eafa3ad --- /dev/null +++ b/src/Ombi/Controllers/V2/IssuesController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using Ombi.Core.Engine.V2; +using Ombi.Store.Entities.Requests; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Ombi.Controllers.V2 +{ + public class IssuesController : V2Controller + { + private readonly IIssuesEngine _engine; + + public IssuesController(IIssuesEngine engine) + { + _engine = engine; + } + + [HttpGet("{position}/{take}/{status}")] + public Task> GetIssuesSummary(int position, int take, IssueStatus status) + { + return _engine.GetIssues(position, take, status, HttpContext.RequestAborted); + } + + + [HttpGet("details/{providerId}")] + public Task GetIssueDetails(string providerId) + { + return _engine.GetIssuesByProviderId(providerId, HttpContext.RequestAborted); + } + } +} diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index 9ef0289e4..91ff09e17 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -216,7 +216,9 @@ "MarkedAsResolved": "This issue has now been marked as resolved!", "MarkedAsInProgress": "This issue has now been marked as in progress!", "Delete": "Delete issue", - "DeletedIssue": "Issue has been deleted" + "DeletedIssue": "Issue has been deleted", + "Chat":"Chat", + "Requested":"Requested" }, "Filter": { "ClearFilter": "Clear Filter",