From 73558f4eaaec99e22385e9926fd8035f8b3afd5f Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Tue, 14 Jul 2026 14:10:55 -0300 Subject: [PATCH] feat: add manual resolve dialog for ingest reviews with image upload --- src/api/generated/api.schemas.ts | 38 +- .../manga-ingest-review.ts | 72 +++ src/components/ui/textarea.tsx | 21 + .../admin/components/ImportReviewCard.tsx | 13 + .../admin/components/ManualResolveDialog.tsx | 469 ++++++++++++++++++ 5 files changed, 612 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/features/admin/components/ManualResolveDialog.tsx diff --git a/src/api/generated/api.schemas.ts b/src/api/generated/api.schemas.ts index 6d8a8f6..0418a7e 100644 --- a/src/api/generated/api.schemas.ts +++ b/src/api/generated/api.schemas.ts @@ -57,6 +57,33 @@ export interface PresignedImportResponseDTO { fileKey?: string; } +export type ManualMangaIngestRequestDTOStatus = typeof ManualMangaIngestRequestDTOStatus[keyof typeof ManualMangaIngestRequestDTOStatus]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const ManualMangaIngestRequestDTOStatus = { + ONGOING: 'ONGOING', + COMPLETED: 'COMPLETED', + HIATUS: 'HIATUS', + CANCELLED: 'CANCELLED', + UNKNOWN: 'UNKNOWN', +} as const; + +export interface ManualMangaIngestRequestDTO { + /** @minLength 1 */ + title: string; + synopsis?: string; + status?: ManualMangaIngestRequestDTOStatus; + score?: number; + publishedFrom?: string; + publishedTo?: string; + chapterCount?: number; + adult?: boolean; + authors?: string[]; + genres?: string[]; + alternativeTitles?: string[]; +} + export interface AuthenticationRequestDTO { email: string; password: string; @@ -234,9 +261,9 @@ export interface PageMangaImportJobDTO { export interface PageableObject { offset?: number; + paged?: boolean; pageNumber?: number; pageSize?: number; - paged?: boolean; unpaged?: boolean; sort?: SortObject; } @@ -422,6 +449,15 @@ malId?: number; aniListId?: number; }; +export type ResolveMangaIngestReviewManuallyParams = { +id: number; +}; + +export type ResolveMangaIngestReviewManuallyBody = { + coverImage: Blob; + request: ManualMangaIngestRequestDTO; +}; + export type GetContentProvidersParams = { manualImport?: boolean; }; diff --git a/src/api/generated/manga-ingest-review/manga-ingest-review.ts b/src/api/generated/manga-ingest-review/manga-ingest-review.ts index e0d349f..6fcb75e 100644 --- a/src/api/generated/manga-ingest-review/manga-ingest-review.ts +++ b/src/api/generated/manga-ingest-review/manga-ingest-review.ts @@ -26,6 +26,8 @@ import type { import type { DefaultResponseDTOListMangaIngestReviewDTO, DefaultResponseDTOVoid, + ResolveMangaIngestReviewManuallyBody, + ResolveMangaIngestReviewManuallyParams, ResolveMangaIngestReviewParams } from '../api.schemas'; @@ -194,6 +196,76 @@ export const useResolveMangaIngestReview = ,signal?: AbortSignal +) => { + + const formData = new FormData(); +formData.append(`coverImage`, resolveMangaIngestReviewManuallyBody.coverImage) +formData.append(`request`, JSON.stringify(resolveMangaIngestReviewManuallyBody.request)); + + return customInstance( + {url: `/catalog/ingest-reviews/manual`, method: 'POST', + headers: {'Content-Type': 'multipart/form-data', }, + data: formData, + params, signal + }, + options); + } + + + +export const getResolveMangaIngestReviewManuallyMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: ResolveMangaIngestReviewManuallyBody;params: ResolveMangaIngestReviewManuallyParams}, TContext>, request?: SecondParameter} +): UseMutationOptions>, TError,{data: ResolveMangaIngestReviewManuallyBody;params: ResolveMangaIngestReviewManuallyParams}, TContext> => { + +const mutationKey = ['resolveMangaIngestReviewManually']; +const {mutation: mutationOptions, request: requestOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, request: undefined}; + + + + + const mutationFn: MutationFunction>, {data: ResolveMangaIngestReviewManuallyBody;params: ResolveMangaIngestReviewManuallyParams}> = (props) => { + const {data,params} = props ?? {}; + + return resolveMangaIngestReviewManually(data,params,requestOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type ResolveMangaIngestReviewManuallyMutationResult = NonNullable>> + export type ResolveMangaIngestReviewManuallyMutationBody = ResolveMangaIngestReviewManuallyBody + export type ResolveMangaIngestReviewManuallyMutationError = unknown + + /** + * @summary Resolve manga ingest review manually + */ +export const useResolveMangaIngestReviewManually = (options?: { mutation?:UseMutationOptions>, TError,{data: ResolveMangaIngestReviewManuallyBody;params: ResolveMangaIngestReviewManuallyParams}, TContext>, request?: SecondParameter} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: ResolveMangaIngestReviewManuallyBody;params: ResolveMangaIngestReviewManuallyParams}, + TContext + > => { + + const mutationOptions = getResolveMangaIngestReviewManuallyMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + /** * Delete pending manga ingest review by ID. * @summary Delete pending manga ingest review */ diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx new file mode 100644 index 0000000..31d7faa --- /dev/null +++ b/src/components/ui/textarea.tsx @@ -0,0 +1,21 @@ +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { + return ( +