From 8913e9cd99b1d184e1684d81dfcc8479b29081f0 Mon Sep 17 00:00:00 2001 From: Rishi Ghan Date: Mon, 9 Mar 2026 11:44:24 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A8=20ComicDetail=20grqphQL=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ComicDetail/ComicDetail.tsx | 31 +-- .../ComicDetail/ComicVineMatchPanel.tsx | 15 +- .../ComicDetail/EditMetadataPanel.tsx | 70 +++---- .../components/ComicDetail/RawFileDetails.tsx | 29 ++- src/client/graphql/generated.ts | 181 ++++++++++++++++++ 5 files changed, 236 insertions(+), 90 deletions(-) diff --git a/src/client/components/ComicDetail/ComicDetail.tsx b/src/client/components/ComicDetail/ComicDetail.tsx index 22973f5..9026f8b 100644 --- a/src/client/components/ComicDetail/ComicDetail.tsx +++ b/src/client/components/ComicDetail/ComicDetail.tsx @@ -67,13 +67,8 @@ type ComicDetailProps = { }; /** - * Component for displaying the metadata for a comic in greater detail. - * - * @component - * @example - * return ( - * - * ) + * Displays full comic detail: cover, file info, action menu, and tabbed panels + * for metadata, archive operations, and acquisition. */ export const ComicDetail = (data: ComicDetailProps): ReactElement => { const { @@ -84,7 +79,6 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { sourcedMetadata: { comicvine, locg, comicInfo }, acquisition, createdAt, - updatedAt, }, userSettings, queryClient, @@ -94,24 +88,10 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => { const [activeTab, setActiveTab] = useState(undefined); const [visible, setVisible] = useState(false); const [slidingPanelContentId, setSlidingPanelContentId] = useState(""); - const [modalIsOpen, setIsOpen] = useState(false); const { comicObjectId } = useParams<{ comicObjectId: string }>(); const { comicVineMatches, prepareAndFetchMatches } = useComicVineMatching(); - // Modal handlers (currently unused but kept for future use) - const openModal = useCallback((filePath: string) => { - setIsOpen(true); - }, []); - - const afterOpenModal = useCallback((things: any) => { - // Modal opened callback - }, []); - - const closeModal = useCallback(() => { - setIsOpen(false); - }, []); - // Action event handlers const openDrawerWithCVMatches = () => { prepareAndFetchMatches(rawFileDetails, comicvine); @@ -224,10 +204,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
{/* action dropdown */} diff --git a/src/client/components/ComicDetail/ComicVineMatchPanel.tsx b/src/client/components/ComicDetail/ComicVineMatchPanel.tsx index 192c4ff..474555b 100644 --- a/src/client/components/ComicDetail/ComicVineMatchPanel.tsx +++ b/src/client/components/ComicDetail/ComicVineMatchPanel.tsx @@ -1,12 +1,21 @@ import React, { ReactElement } from "react"; -import { ComicVineSearchForm } from "../ComicVineSearchForm"; import MatchResult from "./MatchResult"; import { isEmpty } from "lodash"; import { useStore } from "../../store"; import { useShallow } from "zustand/react/shallow"; -export const ComicVineMatchPanel = (comicVineData): ReactElement => { - const { comicObjectId, comicVineMatches, queryClient, onMatchApplied } = comicVineData.props; +interface ComicVineMatchPanelProps { + props: { + comicObjectId: string; + comicVineMatches: any[]; + queryClient?: any; + onMatchApplied?: () => void; + }; +} + +/** Displays ComicVine search results or a status message while searching. */ +export const ComicVineMatchPanel = ({ props: comicVineData }: ComicVineMatchPanelProps): ReactElement => { + const { comicObjectId, comicVineMatches, queryClient, onMatchApplied } = comicVineData; const { comicvine } = useStore( useShallow((state) => ({ comicvine: state.comicvine, diff --git a/src/client/components/ComicDetail/EditMetadataPanel.tsx b/src/client/components/ComicDetail/EditMetadataPanel.tsx index 0e359c8..4d66014 100644 --- a/src/client/components/ComicDetail/EditMetadataPanel.tsx +++ b/src/client/components/ComicDetail/EditMetadataPanel.tsx @@ -1,55 +1,41 @@ -import React, { ReactElement, useCallback, useEffect, useState } from "react"; -import { Form, Field } from "react-final-form"; +import React, { ReactElement } from "react"; +import { Form, Field, FieldRenderProps } from "react-final-form"; import arrayMutators from "final-form-arrays"; import { FieldArray } from "react-final-form-arrays"; import AsyncSelectPaginate from "./AsyncSelectPaginate/AsyncSelectPaginate"; import TextareaAutosize from "react-textarea-autosize"; -export const EditMetadataPanel = (props): ReactElement => { - const validate = async () => {}; +interface EditMetadataPanelProps { + data: { + name?: string; + [key: string]: any; + }; +} + +/** Adapts react-final-form's Field render prop to AsyncSelectPaginate. */ +const AsyncSelectPaginateAdapter = ({ input, ...rest }: FieldRenderProps) => ( + input.onChange(value)} /> +); + +/** Adapts react-final-form's Field render prop to TextareaAutosize. */ +const TextareaAutosizeAdapter = ({ input, ...rest }: FieldRenderProps) => ( + input.onChange(value)} /> +); + +/** Sliding panel form for manually editing comic metadata fields. */ +export const EditMetadataPanel = ({ data }: EditMetadataPanelProps): ReactElement => { const onSubmit = async () => {}; - const { data } = props; - - const AsyncSelectPaginateAdapter = ({ input, ...rest }) => { - return ( - input.onChange(value)} - /> - ); - }; - const TextareaAutosizeAdapter = ({ input, ...rest }) => { - return ( - input.onChange(value)} - /> - ); - }; - // const rawFileDetails = useSelector( - // (state: RootState) => state.comicInfo.comicBookDetail.rawFileDetails.name, - // ); - return ( <>
( {/* Issue Name */} @@ -80,7 +66,6 @@ export const EditMetadataPanel = (props): ReactElement => {

Do not enter the first zero

- {/* year */}
Issue Year
{
- {/* page count */} - {/* Description */}
@@ -113,7 +96,7 @@ export const EditMetadataPanel = (props): ReactElement => { />
-
+
@@ -153,7 +136,7 @@ export const EditMetadataPanel = (props): ReactElement => {
-
+
{/* Publisher */}
@@ -224,7 +207,7 @@ export const EditMetadataPanel = (props): ReactElement => {
-
+
{/* team credits */}
@@ -302,7 +285,6 @@ export const EditMetadataPanel = (props): ReactElement => { )) } -
{JSON.stringify(values, undefined, 2)}
)} /> diff --git a/src/client/components/ComicDetail/RawFileDetails.tsx b/src/client/components/ComicDetail/RawFileDetails.tsx index ce14567..1ee7b7b 100644 --- a/src/client/components/ComicDetail/RawFileDetails.tsx +++ b/src/client/components/ComicDetail/RawFileDetails.tsx @@ -1,29 +1,24 @@ import React, { ReactElement, ReactNode } from "react"; import prettyBytes from "pretty-bytes"; import { isEmpty } from "lodash"; -import { format, parseISO } from "date-fns"; -import { RawFileDetails as RawFileDetailsType } from "../../graphql/generated"; +import { format, parseISO, isValid } from "date-fns"; +import { + RawFileDetails as RawFileDetailsType, + InferredMetadata, +} from "../../graphql/generated"; type RawFileDetailsProps = { data?: { rawFileDetails?: RawFileDetailsType; - inferredMetadata?: { - issue?: { - year?: string; - name?: string; - number?: number; - subtitle?: string; - }; - }; - created_at?: string; - updated_at?: string; + inferredMetadata?: InferredMetadata; + createdAt?: string; }; children?: ReactNode; }; +/** Renders raw file info, inferred metadata, and import timestamp for a comic. */ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => { - const { rawFileDetails, inferredMetadata, created_at, updated_at } = - props.data || {}; + const { rawFileDetails, inferredMetadata, createdAt } = props.data || {}; return ( <>
@@ -97,10 +92,10 @@ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => { Import Details
- {created_at ? ( + {createdAt && isValid(parseISO(createdAt)) ? ( <> - {format(parseISO(created_at), "dd MMMM, yyyy")},{" "} - {format(parseISO(created_at), "h aaaa")} + {format(parseISO(createdAt), "dd MMMM, yyyy")},{" "} + {format(parseISO(createdAt), "h aaaa")} ) : "N/A"}
diff --git a/src/client/graphql/generated.ts b/src/client/graphql/generated.ts index 8d9bc13..65e28ec 100644 --- a/src/client/graphql/generated.ts +++ b/src/client/graphql/generated.ts @@ -28,6 +28,13 @@ export type AcquisitionSourceInput = { wanted?: InputMaybe; }; +export type AppSettings = { + __typename?: 'AppSettings'; + bittorrent?: Maybe; + directConnect?: Maybe; + prowlarr?: Maybe; +}; + export type Archive = { __typename?: 'Archive'; expandedPath?: Maybe; @@ -52,6 +59,26 @@ export type AutoMergeSettingsInput = { onMetadataUpdate?: InputMaybe; }; +export type BittorrentClient = { + __typename?: 'BittorrentClient'; + host?: Maybe; + name?: Maybe; +}; + +export type BittorrentSettings = { + __typename?: 'BittorrentSettings'; + client?: Maybe; +}; + +export type Bundle = { + __typename?: 'Bundle'; + id?: Maybe; + name?: Maybe; + size?: Maybe; + speed?: Maybe; + status?: Maybe; +}; + export type CanonicalMetadata = { __typename?: 'CanonicalMetadata'; ageRating?: Maybe; @@ -123,6 +150,11 @@ export type ComicConnection = { totalCount: Scalars['Int']['output']; }; +export type ComicVineMatchInput = { + volume: ComicVineVolumeRefInput; + volumeInformation?: InputMaybe; +}; + export type ComicVineResourceResponse = { __typename?: 'ComicVineResourceResponse'; error: Scalars['String']['output']; @@ -143,6 +175,24 @@ export type ComicVineSearchResult = { offset: Scalars['Int']['output']; results: Array; status_code: Scalars['Int']['output']; + total: Scalars['Int']['output']; +}; + +export type ComicVineVolume = { + __typename?: 'ComicVineVolume'; + api_detail_url?: Maybe; + count_of_issues?: Maybe; + description?: Maybe; + id?: Maybe; + image?: Maybe; + name?: Maybe; + publisher?: Maybe; + site_detail_url?: Maybe; + start_year?: Maybe; +}; + +export type ComicVineVolumeRefInput = { + api_detail_url: Scalars['String']['input']; }; export enum ConflictResolutionStrategy { @@ -177,10 +227,22 @@ export type DirectConnectBundleInput = { size?: InputMaybe; }; +export type DirectConnectClient = { + __typename?: 'DirectConnectClient'; + airDCPPUserSettings?: Maybe; + host?: Maybe; + hubs?: Maybe>>; +}; + export type DirectConnectInput = { downloads?: InputMaybe>; }; +export type DirectConnectSettings = { + __typename?: 'DirectConnectSettings'; + client?: Maybe; +}; + export type DirectorySize = { __typename?: 'DirectorySize'; fileCount: Scalars['Int']['output']; @@ -234,6 +296,37 @@ export type GetVolumesInput = { volumeURI: Scalars['String']['input']; }; +export type HostConfig = { + __typename?: 'HostConfig'; + hostname?: Maybe; + password?: Maybe; + port?: Maybe; + protocol?: Maybe; + username?: Maybe; +}; + +export type HostInput = { + hostname: Scalars['String']['input']; + password: Scalars['String']['input']; + port: Scalars['String']['input']; + protocol: Scalars['String']['input']; + username: Scalars['String']['input']; +}; + +export type Hub = { + __typename?: 'Hub'; + description?: Maybe; + id?: Maybe; + name?: Maybe; + userCount?: Maybe; +}; + +export type ImageAnalysisResult = { + __typename?: 'ImageAnalysisResult'; + analyzedData?: Maybe; + colorHistogramData?: Maybe; +}; + export type ImageUrls = { __typename?: 'ImageUrls'; icon_url?: Maybe; @@ -516,6 +609,8 @@ export type Mutation = { __typename?: 'Mutation'; /** Placeholder for future mutations */ _empty?: Maybe; + analyzeImage: ImageAnalysisResult; + applyComicVineMatch: Comic; bulkResolveMetadata: Array; forceCompleteSession: ForceCompleteResult; importComic: ImportComicResult; @@ -525,11 +620,23 @@ export type Mutation = { setMetadataField: Comic; startIncrementalImport: IncrementalImportResult; startNewImport: ImportJobResult; + uncompressArchive?: Maybe; updateSourcedMetadata: Comic; updateUserPreferences: UserPreferences; }; +export type MutationAnalyzeImageArgs = { + imageFilePath: Scalars['String']['input']; +}; + + +export type MutationApplyComicVineMatchArgs = { + comicObjectId: Scalars['ID']['input']; + match: ComicVineMatchInput; +}; + + export type MutationBulkResolveMetadataArgs = { comicIds: Array; }; @@ -580,6 +687,13 @@ export type MutationStartNewImportArgs = { }; +export type MutationUncompressArchiveArgs = { + comicObjectId: Scalars['ID']['input']; + filePath: Scalars['String']['input']; + options?: InputMaybe; +}; + + export type MutationUpdateSourcedMetadataArgs = { comicId: Scalars['ID']['input']; metadata: Scalars['String']['input']; @@ -628,6 +742,17 @@ export type Provenance = { url?: Maybe; }; +export type ProwlarrClient = { + __typename?: 'ProwlarrClient'; + apiKey?: Maybe; + host?: Maybe; +}; + +export type ProwlarrSettings = { + __typename?: 'ProwlarrSettings'; + client?: Maybe; +}; + export type Publisher = { __typename?: 'Publisher'; api_detail_url?: Maybe; @@ -644,6 +769,7 @@ export type PublisherStats = { export type Query = { __typename?: 'Query'; analyzeMetadataConflicts: Array; + bundles: Array; comic?: Maybe; comics: ComicConnection; /** Fetch resource from Metron API */ @@ -664,13 +790,18 @@ export type Query = { getVolume: VolumeDetailResponse; /** Get weekly pull list from League of Comic Geeks */ getWeeklyPullList: MetadataPullListResponse; + hubs: Array; previewCanonicalMetadata?: Maybe; /** Search ComicVine for volumes, issues, characters, etc. */ searchComicVine: ComicVineSearchResult; searchIssue: SearchIssueResult; + searchTorrents: Array; + settings?: Maybe; + torrentJobs?: Maybe; userPreferences?: Maybe; /** Advanced volume-based search with scoring and filtering */ volumeBasedSearch: VolumeBasedSearchResponse; + walkFolders: Array; }; @@ -679,6 +810,12 @@ export type QueryAnalyzeMetadataConflictsArgs = { }; +export type QueryBundlesArgs = { + comicObjectId: Scalars['ID']['input']; + config?: InputMaybe; +}; + + export type QueryComicArgs = { id: Scalars['ID']['input']; }; @@ -734,6 +871,11 @@ export type QueryGetWeeklyPullListArgs = { }; +export type QueryHubsArgs = { + host: HostInput; +}; + + export type QueryPreviewCanonicalMetadataArgs = { comicId: Scalars['ID']['input']; preferences?: InputMaybe; @@ -752,6 +894,21 @@ export type QuerySearchIssueArgs = { }; +export type QuerySearchTorrentsArgs = { + query: Scalars['String']['input']; +}; + + +export type QuerySettingsArgs = { + settingsKey?: InputMaybe; +}; + + +export type QueryTorrentJobsArgs = { + trigger: Scalars['String']['input']; +}; + + export type QueryUserPreferencesArgs = { userId?: InputMaybe; }; @@ -761,6 +918,12 @@ export type QueryVolumeBasedSearchArgs = { input: VolumeSearchInput; }; + +export type QueryWalkFoldersArgs = { + basePathToWalk: Scalars['String']['input']; + extensions?: InputMaybe>; +}; + export type RawFileDetails = { __typename?: 'RawFileDetails'; archive?: Maybe; @@ -939,6 +1102,24 @@ export type TeamCredit = { site_detail_url?: Maybe; }; +export type TorrentJob = { + __typename?: 'TorrentJob'; + id?: Maybe; + name?: Maybe; +}; + +export type TorrentSearchResult = { + __typename?: 'TorrentSearchResult'; + downloadUrl?: Maybe; + guid?: Maybe; + indexer?: Maybe; + leechers?: Maybe; + publishDate?: Maybe; + seeders?: Maybe; + size?: Maybe; + title?: Maybe; +}; + export type UserPreferences = { __typename?: 'UserPreferences'; autoMerge: AutoMergeSettings;