Merge pull request #4083 from Ombi-app/feature/issues-rework

Feature/issues rework
pull/4091/head v4.0.1142
Jamie 4 years ago committed by GitHub
commit 5bd64a93e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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<IEnumerable<IssuesSummaryModel>> GetIssues(int position, int take, IssueStatus status, CancellationToken token);
Task<IssuesSummaryModel> GetIssuesByProviderId(string providerId, CancellationToken token);
}
public class IssuesEngine : IIssuesEngine
{
private readonly IRepository<IssueCategory> _categories;
private readonly IRepository<Issues> _issues;
private readonly IRepository<IssueComments> _comments;
public IssuesEngine(IRepository<IssueCategory> categories,
IRepository<Issues> issues,
IRepository<IssueComments> comments)
{
_categories = categories;
_issues = issues;
_comments = comments;
}
public async Task<IEnumerable<IssuesSummaryModel>> 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<IssuesSummaryModel>();
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<IssuesSummaryModel> 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> Issues { get; set; }
}
}

@ -114,6 +114,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<ITVSearchEngineV2, TvSearchEngineV2>();
services.AddTransient<ICalendarEngine, CalendarEngine>();
services.AddTransient<IMusicSearchEngineV2, MusicSearchEngineV2>();
services.AddTransient<IIssuesEngine, IssuesEngine>();
}
public static void RegisterHttp(this IServiceCollection services)

@ -145,6 +145,7 @@ export function JwtTokenGetter() {
MatCheckboxModule,
MatProgressSpinnerModule,
MDBBootstrapModule.forRoot(),
// NbThemeModule.forRoot({ name: 'dark'}),
JwtModule.forRoot({
config: {
tokenGetter: JwtTokenGetter,

@ -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");
}

@ -62,3 +62,10 @@ export interface IUpdateStatus {
issueId: number;
status: IssueStatus;
}
export interface IIssuesSummary {
title: string;
count: number;
providerId: string;
issues: IIssues[];
}

@ -0,0 +1,27 @@
<mat-card class="issue-card" *ngIf="!deleted">
<mat-card-header>
<mat-card-title>{{issue.subject}}</mat-card-title>
<mat-card-subtitle>{{issue.userReported?.userName}} on {{issue.createdDate | date:short}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{issue.description}}
</p>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button (click)="openChat(issue)" color="accent"><i class="far fa-comments"></i> {{'Issues.Chat' | translate }}</button>
<div *ngIf="isAdmin && settings;then content else empty">here is ignored</div>
<ng-template #content>
<button mat-raised-button color="accent"
*ngIf="issue.status === IssueStatus.Pending && settings.enableInProgress"
(click)="inProgress(issue)">{{'Issues.MarkInProgress' | translate }}</button>
<button mat-raised-button color="accent"
*ngIf="issue.status === IssueStatus.Pending && !settings.enableInProgress || issue.status == IssueStatus.InProgress"
(click)="resolve(issue)">{{'Issues.MarkResolved' | translate}}</button>
<button mat-raised-button color="warn" (click)="delete(issue)"><i class="far fa-times-circle"></i> {{'Issues.Delete' | translate}}</button></ng-template>
<ng-template #empty></ng-template>
</mat-card-actions>
</mat-card>

@ -0,0 +1,9 @@
@import "~styles/variables.scss";
::ng-deep .issue-card {
border: 3px solid $ombi-background-primary-accent;
}
.top-spacing {
margin-top:2%;
}

@ -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;
});
}
}

@ -0,0 +1,20 @@
<div class="container top-spacing" *ngIf="details">
<h1>Issues for {{details.title}}</h1>
<div>
<span>{{'Issues.Requested' | translate}}
<i *ngIf="!hasRequest" class="far fa-times-circle"></i>
<i *ngIf="hasRequest" class="far fa-check-circle"></i>
</span>
<div>
<button mat-raised-button color="accent" (click)="navToMedia()"><i class="far fa-eye"></i> {{'Common.ViewDetails' | translate }}</button>
</div>
<div class="row">
<div class="col-6 top-spacing" *ngFor="let issue of details.issues">
<issues-details-group [issue]="issue" [settings]="settings" [isAdmin]="isAdmin"></issues-details-group>
</div>
</div>
</div>
<div class="row">
</div>
</div>

@ -0,0 +1,9 @@
@import "~styles/variables.scss";
::ng-deep .mat-card {
background: $ombi-background-primary-accent;
}
.top-spacing {
margin-top:2%;
}

@ -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;
}
}
}

@ -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,
];

@ -0,0 +1,6 @@
<ombi-chat-box *ngIf="loaded"
[messages]="messages"
(onAddMessage)="addComment($event)"
>
</ombi-chat-box>

@ -0,0 +1,9 @@
@import "~styles/variables.scss";
::ng-deep .mat-card {
background: $ombi-background-primary-accent;
}
.top-spacing {
margin-top:2%;
}

@ -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<IssueChatComponent>,
@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
});
});
}
}

@ -1,21 +0,0 @@
<!-- <table mat-table [dataSource]="pendingIssues" multiTemplateDataRows class="mat-elevation-z8">
<ng-container matColumnDef="{{column}}" *ngFor="let column of columnsToDisplay">
<th mat-header-cell *matHeaderCellDef> {{column}} </th>
<td mat-cell *matCellDef="let element"> {{element[column]}} </td>
</ng-container>
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let element" [attr.colspan]="columnsToDisplay.length">
<div class="example-element-detail" [@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'">
<div class="example-element-diagram">
<div class="example-element-position"> {{element.requestId}} </div>
</div>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
<tr mat-row *matRowDef="let element; columns: columnsToDisplay;" class="example-element-row" [class.example-expanded-row]="expandedElement === element" (click)="expandedElement = expandedElement === element ? null : element">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="example-detail-row"></tr>
</table> -->

@ -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;
});
}
}

@ -84,7 +84,7 @@
<div class="col-md-12">
<div *ngIf="isAdmin && settings">
<div *ngIf="issue.status === IssueStatus.Pending && settings.enableInProgress">
<button class="btn btn-primary btn-sm bottom-btn" (click)="inProgress()" [translate]="'Issues.MarkInProgress'"></button>
<button class="btn btn-primary btn-sm bottom-btn" (click)="inProgress()">{{'Issues.MarkInProgress' | translate }}</button>
</div>
<div *ngIf="issue.status === IssueStatus.Pending && !settings.enableInProgress || issue.status == IssueStatus.InProgress">
<button class="btn btn-primary btn-sm bottom-btn" (click)="resolve()" [translate]="'Issues.MarkResolved'"></button>

@ -1,25 +1,28 @@
<div class="small-middle-container">
<mat-tab-group>
<mat-tab label="{{'Issues.PendingTitle' | translate}}">
<ng-template matTabContent>
<div *ngIf="pendingIssues">
<issues-table [issues]="pendingIssues" (changePage)="changePagePending($event)" [totalRecords]="count.pending"></issues-table>
</div>
</ng-template>
</mat-tab>
<mat-tab *ngIf="inProgressIssues.length > 0" label="{{'Issues.InProgressTitle' | translate}}">
<ng-template matTabContent>
<div *ngIf="inProgressIssues">
<issues-table [issues]="inProgressIssues" (changePage)="changePageInProg($event)" [totalRecords]="count.inProgress"></issues-table>
</div>
</ng-template>
</mat-tab>
<mat-tab label="{{'Issues.ResolvedTitle' | translate}}">
<ng-template matTabContent>
<div *ngIf="resolvedIssues">
<issues-table [issues]="resolvedIssues" (changePage)="changePageResolved($event)" [totalRecords]="count.resolved"></issues-table>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
<div *ngIf="count">
<mat-tab-group>
<mat-tab label="{{'Issues.PendingTitle' | translate}}">
<ng-template matTabContent>
<div *ngIf="pendingIssues.length > 0">
<issues-table [issues]="pendingIssues" (changePage)="changePagePending($event)" [totalRecords]="count.pending"></issues-table>
</div>
</ng-template>
</mat-tab>
<mat-tab *ngIf="inProgressIssues.length > 0" label="{{'Issues.InProgressTitle' | translate}}">
<ng-template matTabContent>
<div *ngIf="inProgressIssues">
<issues-table [issues]="inProgressIssues" (changePage)="changePageInProg($event)" [totalRecords]="count.inProgress"></issues-table>
</div>
</ng-template>
</mat-tab>
<mat-tab label="{{'Issues.ResolvedTitle' | translate}}">
<ng-template matTabContent>
<div *ngIf="resolvedIssues.length > 0">
<issues-table [issues]="resolvedIssues" (changePage)="changePageResolved($event)" [totalRecords]="count.resolved"></issues-table>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</div>
</div>

@ -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;
});
}

@ -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
],
})

@ -6,32 +6,15 @@
<td mat-cell *matCellDef="let element"> {{element.title}} </td>
</ng-container>
<ng-container matColumnDef="category">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Issues.Category' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.issueCategory.value}} </td>
</ng-container>
<ng-container matColumnDef="subject">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Issues.Subject' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.subject}} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Issues.Status' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{IssueStatus[element.status] | humanize}} </td>
</ng-container>
<ng-container matColumnDef="reportedBy">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Issues.ReportedBy' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.userReported.userAlias}} </td>
<ng-container matColumnDef="count">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Issues.Count' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.count}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button *ngIf="element.requestType === 1" mat-raised-button color="accent" [routerLink]="'/details/movie/' + element.providerId">{{ 'Issues.Details' | translate}}</button>
<button *ngIf="element.requestType === 0" mat-raised-button color="accent" [routerLink]="'/details/tv/' + element.providerId">{{ 'Issues.Details' | translate}}</button>
<button *ngIf="element.requestType === 2" mat-raised-button color="accent" [routerLink]="'/details/artist/request/' + element.providerId">{{ 'Issues.Details' | translate}}</button>
<button mat-raised-button color="accent" [routerLink]="[element.providerId]">{{ 'Issues.Details' | translate}}</button>
</td>
</ng-container>

@ -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<IPagenator>();
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);
}
}

@ -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,

@ -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 }
});
}

@ -13,6 +13,11 @@
<!-- content start -->
<mat-accordion class="mat-elevation-z8">
<div *ngFor="let issue of issues">
<issues-details-group [issue]="issue" [settings]="settings" [isAdmin]="isAdmin"></issues-details-group>
</div>
<!--
<mat-expansion-panel *ngFor="let issue of issues">
<mat-expansion-panel-header>
<mat-panel-title>
@ -54,7 +59,7 @@
<button mat-raised-button color="warn" (click)="delete(issue)"> {{ 'Issues.Delete' | translate }}</button>
</div>
</div>
</mat-expansion-panel>
</mat-expansion-panel> -->
</mat-accordion>
<!-- content end -->

@ -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;

@ -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<void> {
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"));

@ -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<IIssuesSummary[]> {
return this.http.get<IIssuesSummary[]>(`${this.url}${position}/${take}/${status}`, {headers: this.headers});
}
public getIssuesByProviderId(providerId: string): Observable<IIssuesSummary> {
return this.http.get<IIssuesSummary>(`${this.url}details/${providerId}`, {headers: this.headers});
}
}

@ -0,0 +1,23 @@
<div class='container'>
<div class='chatbox'>
<div class='chatbox__user-list'>
<h1>Users</h1>
<div class='chatbox__user--active' *ngFor="let user of userList">
<p>{{user}}</p>
</div>
</div>
<div class="chatbox__messages" *ngFor="let m of messages">
<div class="chatbox__messages__user-message">
<div class="chatbox__messages__user-message--ind-message" [ngClass]="{'sender': m.chatType === ChatType.Sender, 'reciever':m.chatType === ChatType.Reciever }">
<p class="name" *ngIf="m?.username">{{m.username}}</p>
<br/>
<p class="message">{{m.message}}</p>
<p class="timestamp">{{m.date | amLocal | amDateFormat: 'l LT'}}</p>
</div>
</div>
</div>
<div class="form">
<input type="text" [(ngModel)]="currentMessage" placeholder="Enter your message">
<button mat-raised-button class="add-message" (click)="addMessage()">Send</button>
</div>
</div>

@ -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;
// }

@ -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<string> = new EventEmitter<string>();
@Output() onDeleteMessage: EventEmitter<number> = new EventEmitter<number>();
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 = '';
}
}
}

@ -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,

@ -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,

@ -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<IEnumerable<IssuesSummaryModel>> GetIssuesSummary(int position, int take, IssueStatus status)
{
return _engine.GetIssues(position, take, status, HttpContext.RequestAborted);
}
[HttpGet("details/{providerId}")]
public Task<IssuesSummaryModel> GetIssueDetails(string providerId)
{
return _engine.GetIssuesByProviderId(providerId, HttpContext.RequestAborted);
}
}
}

@ -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",

Loading…
Cancel
Save