🔨 ComicDetail grqphQL refactor

This commit is contained in:
2026-03-09 11:44:24 -04:00
parent c392333170
commit 8913e9cd99
5 changed files with 236 additions and 90 deletions

View File

@@ -67,13 +67,8 @@ type ComicDetailProps = {
};
/**
* Component for displaying the metadata for a comic in greater detail.
*
* @component
* @example
* return (
* <ComicDetail/>
* )
* 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<number | undefined>(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 => {
<div className="grid">
<RawFileDetails
data={{
rawFileDetails: rawFileDetails,
inferredMetadata: inferredMetadata,
created_at: createdAt,
updated_at: updatedAt,
rawFileDetails,
inferredMetadata,
createdAt,
}}
>
{/* action dropdown */}

View File

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

View File

@@ -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<any>) => (
<AsyncSelectPaginate {...input} {...rest} onChange={(value) => input.onChange(value)} />
);
/** Adapts react-final-form's Field render prop to TextareaAutosize. */
const TextareaAutosizeAdapter = ({ input, ...rest }: FieldRenderProps<any>) => (
<TextareaAutosize {...input} {...rest} onChange={(value) => 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 (
<AsyncSelectPaginate
{...input}
{...rest}
onChange={(value) => input.onChange(value)}
/>
);
};
const TextareaAutosizeAdapter = ({ input, ...rest }) => {
return (
<TextareaAutosize
{...input}
{...rest}
onChange={(value) => input.onChange(value)}
/>
);
};
// const rawFileDetails = useSelector(
// (state: RootState) => state.comicInfo.comicBookDetail.rawFileDetails.name,
// );
return (
<>
<Form
onSubmit={onSubmit}
validate={validate}
mutators={{
...arrayMutators,
}}
mutators={{ ...arrayMutators }}
render={({
handleSubmit,
form: {
mutators: { push, pop },
}, // injected from final-form-arrays above
pristine,
form,
submitting,
values,
},
}) => (
<form onSubmit={handleSubmit}>
{/* Issue Name */}
@@ -80,7 +66,6 @@ export const EditMetadataPanel = (props): ReactElement => {
<p className="text-xs">Do not enter the first zero</p>
</div>
<div>
{/* year */}
<div className="text-sm">Issue Year</div>
<Field
name="issue_year"
@@ -100,8 +85,6 @@ export const EditMetadataPanel = (props): ReactElement => {
</div>
</div>
{/* page count */}
{/* Description */}
<div className="mt-2">
<label className="text-sm">Description</label>
@@ -113,7 +96,7 @@ export const EditMetadataPanel = (props): ReactElement => {
/>
</div>
<hr size="1" />
<hr />
<div className="field is-horizontal">
<div className="field-label">
@@ -153,7 +136,7 @@ export const EditMetadataPanel = (props): ReactElement => {
</div>
</div>
<hr size="1" />
<hr />
{/* Publisher */}
<div className="field is-horizontal">
@@ -224,7 +207,7 @@ export const EditMetadataPanel = (props): ReactElement => {
</div>
</div>
<hr size="1" />
<hr />
{/* team credits */}
<div className="field is-horizontal">
@@ -302,7 +285,6 @@ export const EditMetadataPanel = (props): ReactElement => {
))
}
</FieldArray>
<pre>{JSON.stringify(values, undefined, 2)}</pre>
</form>
)}
/>

View File

@@ -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 (
<>
<div className="max-w-2xl ml-5">
@@ -97,10 +92,10 @@ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
Import Details
</dt>
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
{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"}
</dd>

View File

@@ -28,6 +28,13 @@ export type AcquisitionSourceInput = {
wanted?: InputMaybe<Scalars['Boolean']['input']>;
};
export type AppSettings = {
__typename?: 'AppSettings';
bittorrent?: Maybe<BittorrentSettings>;
directConnect?: Maybe<DirectConnectSettings>;
prowlarr?: Maybe<ProwlarrSettings>;
};
export type Archive = {
__typename?: 'Archive';
expandedPath?: Maybe<Scalars['String']['output']>;
@@ -52,6 +59,26 @@ export type AutoMergeSettingsInput = {
onMetadataUpdate?: InputMaybe<Scalars['Boolean']['input']>;
};
export type BittorrentClient = {
__typename?: 'BittorrentClient';
host?: Maybe<HostConfig>;
name?: Maybe<Scalars['String']['output']>;
};
export type BittorrentSettings = {
__typename?: 'BittorrentSettings';
client?: Maybe<BittorrentClient>;
};
export type Bundle = {
__typename?: 'Bundle';
id?: Maybe<Scalars['Int']['output']>;
name?: Maybe<Scalars['String']['output']>;
size?: Maybe<Scalars['String']['output']>;
speed?: Maybe<Scalars['String']['output']>;
status?: Maybe<Scalars['String']['output']>;
};
export type CanonicalMetadata = {
__typename?: 'CanonicalMetadata';
ageRating?: Maybe<MetadataField>;
@@ -123,6 +150,11 @@ export type ComicConnection = {
totalCount: Scalars['Int']['output'];
};
export type ComicVineMatchInput = {
volume: ComicVineVolumeRefInput;
volumeInformation?: InputMaybe<Scalars['JSON']['input']>;
};
export type ComicVineResourceResponse = {
__typename?: 'ComicVineResourceResponse';
error: Scalars['String']['output'];
@@ -143,6 +175,24 @@ export type ComicVineSearchResult = {
offset: Scalars['Int']['output'];
results: Array<SearchResultItem>;
status_code: Scalars['Int']['output'];
total: Scalars['Int']['output'];
};
export type ComicVineVolume = {
__typename?: 'ComicVineVolume';
api_detail_url?: Maybe<Scalars['String']['output']>;
count_of_issues?: Maybe<Scalars['Int']['output']>;
description?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['Int']['output']>;
image?: Maybe<VolumeImage>;
name?: Maybe<Scalars['String']['output']>;
publisher?: Maybe<Publisher>;
site_detail_url?: Maybe<Scalars['String']['output']>;
start_year?: Maybe<Scalars['String']['output']>;
};
export type ComicVineVolumeRefInput = {
api_detail_url: Scalars['String']['input'];
};
export enum ConflictResolutionStrategy {
@@ -177,10 +227,22 @@ export type DirectConnectBundleInput = {
size?: InputMaybe<Scalars['String']['input']>;
};
export type DirectConnectClient = {
__typename?: 'DirectConnectClient';
airDCPPUserSettings?: Maybe<Scalars['JSON']['output']>;
host?: Maybe<HostConfig>;
hubs?: Maybe<Array<Maybe<Scalars['JSON']['output']>>>;
};
export type DirectConnectInput = {
downloads?: InputMaybe<Array<DirectConnectBundleInput>>;
};
export type DirectConnectSettings = {
__typename?: 'DirectConnectSettings';
client?: Maybe<DirectConnectClient>;
};
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<Scalars['String']['output']>;
password?: Maybe<Scalars['String']['output']>;
port?: Maybe<Scalars['String']['output']>;
protocol?: Maybe<Scalars['String']['output']>;
username?: Maybe<Scalars['String']['output']>;
};
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<Scalars['String']['output']>;
id?: Maybe<Scalars['Int']['output']>;
name?: Maybe<Scalars['String']['output']>;
userCount?: Maybe<Scalars['Int']['output']>;
};
export type ImageAnalysisResult = {
__typename?: 'ImageAnalysisResult';
analyzedData?: Maybe<Scalars['JSON']['output']>;
colorHistogramData?: Maybe<Scalars['JSON']['output']>;
};
export type ImageUrls = {
__typename?: 'ImageUrls';
icon_url?: Maybe<Scalars['String']['output']>;
@@ -516,6 +609,8 @@ export type Mutation = {
__typename?: 'Mutation';
/** Placeholder for future mutations */
_empty?: Maybe<Scalars['String']['output']>;
analyzeImage: ImageAnalysisResult;
applyComicVineMatch: Comic;
bulkResolveMetadata: Array<Comic>;
forceCompleteSession: ForceCompleteResult;
importComic: ImportComicResult;
@@ -525,11 +620,23 @@ export type Mutation = {
setMetadataField: Comic;
startIncrementalImport: IncrementalImportResult;
startNewImport: ImportJobResult;
uncompressArchive?: Maybe<Scalars['Boolean']['output']>;
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<Scalars['ID']['input']>;
};
@@ -580,6 +687,13 @@ export type MutationStartNewImportArgs = {
};
export type MutationUncompressArchiveArgs = {
comicObjectId: Scalars['ID']['input'];
filePath: Scalars['String']['input'];
options?: InputMaybe<Scalars['JSON']['input']>;
};
export type MutationUpdateSourcedMetadataArgs = {
comicId: Scalars['ID']['input'];
metadata: Scalars['String']['input'];
@@ -628,6 +742,17 @@ export type Provenance = {
url?: Maybe<Scalars['String']['output']>;
};
export type ProwlarrClient = {
__typename?: 'ProwlarrClient';
apiKey?: Maybe<Scalars['String']['output']>;
host?: Maybe<HostConfig>;
};
export type ProwlarrSettings = {
__typename?: 'ProwlarrSettings';
client?: Maybe<ProwlarrClient>;
};
export type Publisher = {
__typename?: 'Publisher';
api_detail_url?: Maybe<Scalars['String']['output']>;
@@ -644,6 +769,7 @@ export type PublisherStats = {
export type Query = {
__typename?: 'Query';
analyzeMetadataConflicts: Array<MetadataConflict>;
bundles: Array<Bundle>;
comic?: Maybe<Comic>;
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<Hub>;
previewCanonicalMetadata?: Maybe<CanonicalMetadata>;
/** Search ComicVine for volumes, issues, characters, etc. */
searchComicVine: ComicVineSearchResult;
searchIssue: SearchIssueResult;
searchTorrents: Array<TorrentSearchResult>;
settings?: Maybe<AppSettings>;
torrentJobs?: Maybe<TorrentJob>;
userPreferences?: Maybe<UserPreferences>;
/** Advanced volume-based search with scoring and filtering */
volumeBasedSearch: VolumeBasedSearchResponse;
walkFolders: Array<Scalars['String']['output']>;
};
@@ -679,6 +810,12 @@ export type QueryAnalyzeMetadataConflictsArgs = {
};
export type QueryBundlesArgs = {
comicObjectId: Scalars['ID']['input'];
config?: InputMaybe<Scalars['JSON']['input']>;
};
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<UserPreferencesInput>;
@@ -752,6 +894,21 @@ export type QuerySearchIssueArgs = {
};
export type QuerySearchTorrentsArgs = {
query: Scalars['String']['input'];
};
export type QuerySettingsArgs = {
settingsKey?: InputMaybe<Scalars['String']['input']>;
};
export type QueryTorrentJobsArgs = {
trigger: Scalars['String']['input'];
};
export type QueryUserPreferencesArgs = {
userId?: InputMaybe<Scalars['String']['input']>;
};
@@ -761,6 +918,12 @@ export type QueryVolumeBasedSearchArgs = {
input: VolumeSearchInput;
};
export type QueryWalkFoldersArgs = {
basePathToWalk: Scalars['String']['input'];
extensions?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type RawFileDetails = {
__typename?: 'RawFileDetails';
archive?: Maybe<Archive>;
@@ -939,6 +1102,24 @@ export type TeamCredit = {
site_detail_url?: Maybe<Scalars['String']['output']>;
};
export type TorrentJob = {
__typename?: 'TorrentJob';
id?: Maybe<Scalars['String']['output']>;
name?: Maybe<Scalars['String']['output']>;
};
export type TorrentSearchResult = {
__typename?: 'TorrentSearchResult';
downloadUrl?: Maybe<Scalars['String']['output']>;
guid?: Maybe<Scalars['String']['output']>;
indexer?: Maybe<Scalars['String']['output']>;
leechers?: Maybe<Scalars['Int']['output']>;
publishDate?: Maybe<Scalars['String']['output']>;
seeders?: Maybe<Scalars['Int']['output']>;
size?: Maybe<Scalars['Float']['output']>;
title?: Maybe<Scalars['String']['output']>;
};
export type UserPreferences = {
__typename?: 'UserPreferences';
autoMerge: AutoMergeSettings;