mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
More Testing (#2683)
This commit is contained in:
parent
4ce2b4343a
commit
685f7365e1
@ -61,4 +61,23 @@ public class ReviewController : BaseApiController
|
||||
_scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body));
|
||||
return Ok(_mapper.Map<UserReviewDto>(rating));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the user's review for the given series
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteReview(int seriesId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList();
|
||||
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
@ -132,20 +132,31 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
var seriesDetailPlusDto = new SeriesDetailPlusDto()
|
||||
IEnumerable<UserReviewDto> reviews = new List<UserReviewDto>();
|
||||
if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any())
|
||||
{
|
||||
Ratings = seriesDetailDto.ExternalRatings
|
||||
.DefaultIfEmpty()
|
||||
.Select(r => _mapper.Map<RatingDto>(r)),
|
||||
Reviews = seriesDetailDto.ExternalReviews
|
||||
.DefaultIfEmpty()
|
||||
.OrderByDescending(r => r.Score)
|
||||
reviews = seriesDetailDto.ExternalReviews
|
||||
.Select(r =>
|
||||
{
|
||||
var ret = _mapper.Map<UserReviewDto>(r);
|
||||
ret.IsExternal = true;
|
||||
return ret;
|
||||
}),
|
||||
})
|
||||
.OrderByDescending(r => r.Score);
|
||||
}
|
||||
|
||||
IEnumerable<RatingDto> ratings = new List<RatingDto>();
|
||||
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Any())
|
||||
{
|
||||
ratings = seriesDetailDto.ExternalRatings
|
||||
.Select(r => _mapper.Map<RatingDto>(r));
|
||||
}
|
||||
|
||||
|
||||
var seriesDetailPlusDto = new SeriesDetailPlusDto()
|
||||
{
|
||||
Ratings = ratings,
|
||||
Reviews = reviews,
|
||||
Recommendations = new RecommendationDto()
|
||||
{
|
||||
ExternalSeries = externalSeriesRecommendations,
|
||||
|
@ -114,7 +114,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
try
|
||||
{
|
||||
var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value;
|
||||
var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/series-detail")
|
||||
var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail")
|
||||
.WithHeader("Accept", "application/json")
|
||||
.WithHeader("User-Agent", "Kavita")
|
||||
.WithHeader("x-license-key", license)
|
||||
@ -298,7 +298,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
||||
}
|
||||
try
|
||||
{
|
||||
return await (Configuration.KavitaPlusApiUrl + "/api/metadata/series/detail")
|
||||
return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids")
|
||||
.WithHeader("Accept", "application/json")
|
||||
.WithHeader("User-Agent", "Kavita")
|
||||
.WithHeader("x-license-key", license)
|
||||
|
@ -10,7 +10,7 @@ namespace API.Services;
|
||||
|
||||
public static class ReviewService
|
||||
{
|
||||
public static IList<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
|
||||
public static IEnumerable<UserReviewDto> SelectSpectrumOfReviews(IList<UserReviewDto> reviews)
|
||||
{
|
||||
IList<UserReviewDto> externalReviews;
|
||||
var totalReviews = reviews.Count;
|
||||
@ -42,8 +42,9 @@ public static class ReviewService
|
||||
externalReviews = reviews;
|
||||
}
|
||||
|
||||
return externalReviews;
|
||||
return externalReviews.OrderByDescending(r => r.Score);
|
||||
}
|
||||
|
||||
public static string GetCharacters(string body)
|
||||
{
|
||||
if (string.IsNullOrEmpty(body)) return body;
|
||||
|
@ -171,7 +171,7 @@ public class SeriesService : ISeriesService
|
||||
} else
|
||||
{
|
||||
hasWebLinksChanged =
|
||||
series.Metadata.WebLinks.Equals(updateSeriesMetadataDto.SeriesMetadata?.WebLinks);
|
||||
series.Metadata.WebLinks != updateSeriesMetadataDto.SeriesMetadata?.WebLinks;
|
||||
series.Metadata.WebLinks = string.Join(",", updateSeriesMetadataDto.SeriesMetadata?.WebLinks
|
||||
.Split(",")
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
|
@ -209,16 +209,21 @@ export class SeriesService {
|
||||
return this.httpClient.get<SeriesDetail>(this.baseUrl + 'series/series-detail?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
getReviews(seriesId: number) {
|
||||
return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'review?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
|
||||
deleteReview(seriesId: number) {
|
||||
return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId);
|
||||
}
|
||||
updateReview(seriesId: number, body: string) {
|
||||
return this.httpClient.post<UserReview>(this.baseUrl + 'review', {
|
||||
seriesId, body
|
||||
});
|
||||
}
|
||||
|
||||
getReviews(seriesId: number) {
|
||||
return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'review?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
getRatings(seriesId: number) {
|
||||
return this.httpClient.get<Array<Rating>>(this.baseUrl + 'rating?seriesId=' + seriesId);
|
||||
}
|
||||
|
@ -1,10 +1,23 @@
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {UserReview} from "./user-review";
|
||||
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.component";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {ReviewSeriesModalComponent} from "../review-series-modal/review-series-modal.component";
|
||||
import {
|
||||
ReviewSeriesModalCloseAction,
|
||||
ReviewSeriesModalCloseEvent,
|
||||
ReviewSeriesModalComponent
|
||||
} from "../review-series-modal/review-series-modal.component";
|
||||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
@ -25,6 +38,7 @@ export class ReviewCardComponent implements OnInit {
|
||||
protected readonly ScrobbleProvider = ScrobbleProvider;
|
||||
|
||||
@Input({required: true}) review!: UserReview;
|
||||
@Output() refresh = new EventEmitter<ReviewSeriesModalCloseEvent>();
|
||||
|
||||
isMyReview: boolean = false;
|
||||
|
||||
@ -48,5 +62,10 @@ export class ReviewCardComponent implements OnInit {
|
||||
}
|
||||
const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'});
|
||||
ref.componentInstance.review = this.review;
|
||||
ref.closed.subscribe((res: ReviewSeriesModalCloseEvent | undefined) => {
|
||||
if (res) {
|
||||
this.refresh.emit(res);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger" (click)="delete()">{{t('delete')}}</button>
|
||||
<button class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="save()">{{t('save')}}</button>
|
||||
</div>
|
||||
|
@ -12,7 +12,21 @@ import {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import {UserReview} from "../review-card/user-review";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {ConfirmService} from "../../shared/confirm.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
export enum ReviewSeriesModalCloseAction {
|
||||
Create,
|
||||
Edit,
|
||||
Delete,
|
||||
Close
|
||||
}
|
||||
export interface ReviewSeriesModalCloseEvent {
|
||||
success: boolean,
|
||||
review: UserReview;
|
||||
action: ReviewSeriesModalCloseAction
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-review-series-modal',
|
||||
@ -27,6 +41,8 @@ export class ReviewSeriesModalComponent implements OnInit {
|
||||
protected readonly modal = inject(NgbActiveModal);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
protected readonly minLength = 5;
|
||||
|
||||
@Input({required: true}) review!: UserReview;
|
||||
@ -40,16 +56,23 @@ export class ReviewSeriesModalComponent implements OnInit {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close({success: false, review: null});
|
||||
this.modal.close({success: false, review: this.review, action: ReviewSeriesModalCloseAction.Close});
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (!await this.confirmService.confirm(translate('toasts.delete-review'))) return;
|
||||
this.seriesService.deleteReview(this.review.seriesId).subscribe(() => {
|
||||
this.toastr.success(translate('toasts.review-deleted'));
|
||||
this.modal.close({success: true, review: this.review, action: ReviewSeriesModalCloseAction.Delete});
|
||||
});
|
||||
}
|
||||
save() {
|
||||
const model = this.reviewGroup.value;
|
||||
if (model.reviewBody.length < this.minLength) {
|
||||
return;
|
||||
}
|
||||
this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(review => {
|
||||
this.modal.close({success: true, review: review});
|
||||
this.modal.close({success: true, review: review, action: ReviewSeriesModalCloseAction.Edit});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +138,7 @@
|
||||
iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}"
|
||||
[clickableTitle]="true" (sectionClick)="openReviewModal()">
|
||||
<ng-template #carouselItem let-item let-position="idx">
|
||||
<app-review-card [review]="item"></app-review-card>
|
||||
<app-review-card [review]="item" (refresh)="updateOrDeleteReview($event)"></app-review-card>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</div>
|
||||
|
@ -4,7 +4,8 @@ import {
|
||||
DOCUMENT,
|
||||
NgClass,
|
||||
NgFor,
|
||||
NgIf, NgOptimizedImage,
|
||||
NgIf,
|
||||
NgOptimizedImage,
|
||||
NgStyle,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
@ -44,7 +45,7 @@ import {
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {catchError, forkJoin, Observable, of} from 'rxjs';
|
||||
import {filter, map, take, tap} from 'rxjs/operators';
|
||||
import {map, take} from 'rxjs/operators';
|
||||
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
|
||||
import {CardDetailDrawerComponent} from 'src/app/cards/card-detail-drawer/card-detail-drawer.component';
|
||||
import {EditSeriesModalComponent} from 'src/app/cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||
@ -75,7 +76,11 @@ import {ReaderService} from 'src/app/_services/reader.service';
|
||||
import {ReadingListService} from 'src/app/_services/reading-list.service';
|
||||
import {ScrollService} from 'src/app/_services/scroll.service';
|
||||
import {SeriesService} from 'src/app/_services/series.service';
|
||||
import {ReviewSeriesModalComponent} from '../../../_single-module/review-series-modal/review-series-modal.component';
|
||||
import {
|
||||
ReviewSeriesModalCloseAction,
|
||||
ReviewSeriesModalCloseEvent,
|
||||
ReviewSeriesModalComponent
|
||||
} from '../../../_single-module/review-series-modal/review-series-modal.component';
|
||||
import {PageLayoutMode} from 'src/app/_models/page-layout-mode';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {UserReview} from "../../../_single-module/review-card/user-review";
|
||||
@ -137,7 +142,13 @@ const KavitaPlusSupportedLibraryTypes = [LibraryType.Manga, LibraryType.Book];
|
||||
styleUrls: ['./series-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent, NgClass, NgOptimizedImage, ProviderImagePipe, AsyncPipe]
|
||||
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle,
|
||||
TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu,
|
||||
NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent,
|
||||
NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent,
|
||||
EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet,
|
||||
LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent,
|
||||
NgClass, NgOptimizedImage, ProviderImagePipe, AsyncPipe]
|
||||
})
|
||||
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
@ -704,7 +715,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
if (data.ratings) {
|
||||
this.ratings = [...data.ratings];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Recommendations
|
||||
if (data.recommendations) {
|
||||
@ -854,18 +865,36 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
}
|
||||
|
||||
modalRef.closed.subscribe((closeResult) => {
|
||||
// BUG: This never executes!
|
||||
console.log('Close Result: ')
|
||||
if (closeResult.success && closeResult.review !== null) {
|
||||
const index = this.reviews.findIndex(r => r.username === closeResult.review!.username);
|
||||
console.log('update index: ', index, ' with review ', closeResult.review);
|
||||
this.reviews[index] = closeResult.review;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
this.updateOrDeleteReview(closeResult);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
updateOrDeleteReview(closeResult: ReviewSeriesModalCloseEvent) {
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Close) return;
|
||||
|
||||
const index = this.reviews.findIndex(r => r.username === closeResult.review!.username);
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Edit) {
|
||||
if (index === -1 ) {
|
||||
// A new series was added:
|
||||
this.reviews = [closeResult.review, ...this.reviews];
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
// An edit occurred
|
||||
this.reviews[index] = closeResult.review;
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if (closeResult.action === ReviewSeriesModalCloseAction.Delete) {
|
||||
// An edit occurred
|
||||
this.reviews = [...this.reviews.filter(r => r.username !== closeResult.review!.username)];
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
|
@ -65,6 +65,7 @@
|
||||
"review-label": "Review",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Review must be at least {{count}} characters",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
@ -2011,6 +2012,8 @@
|
||||
"mark-unread": "Marked as Unread",
|
||||
"series-removed-want-to-read": "Series removed from Want to Read list",
|
||||
"series-deleted": "Series deleted",
|
||||
"delete-review": "Are you sure you want to delete your review?",
|
||||
"review-deleted": "Review deleted",
|
||||
"file-send-to": "File(s) emailed to {{name}}",
|
||||
"theme-missing": "The active theme no longer exists. Please refresh the page.",
|
||||
"email-sent": "Email sent to {{email}}",
|
||||
|
23
openapi.json
23
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.13.15"
|
||||
"version": "0.7.13.16"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -7193,6 +7193,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Review"
|
||||
],
|
||||
"summary": "Deletes the user's review for the given series",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "seriesId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Scrobbling/anilist-token": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user