🔨 Added JsDoc, WIP metadata reconciliation

This commit is contained in:
2026-04-02 18:29:40 -04:00
parent d7ab553120
commit 17db1e64e1
5 changed files with 432 additions and 44 deletions

View File

@@ -10,7 +10,7 @@ import "react-sliding-pane/dist/react-sliding-pane.css";
import SlidingPane from "react-sliding-pane";
import { determineCoverFile } from "../../shared/utils/metadata.utils";
import { styled } from "styled-components";
import { RawFileDetails as RawFileDetailsType } from "../../graphql/generated";
import type { RawFileDetails as RawFileDetailsType, InferredMetadata } from "../../graphql/generated";
// Extracted modules
import { useComicVineMatching } from "./useComicVineMatching";
@@ -23,52 +23,47 @@ const StyledSlidingPanel = styled(SlidingPane)`
background: #ccc;
`;
type InferredIssue = {
interface ComicVineMetadata {
name?: string;
number?: number;
year?: string;
subtitle?: string;
[key: string]: any;
};
volumeInformation?: Record<string, unknown>;
[key: string]: unknown;
}
type ComicVineMetadata = {
name?: string;
volumeInformation?: any;
[key: string]: any;
};
type Acquisition = {
interface Acquisition {
directconnect?: {
downloads?: any[];
downloads?: unknown[];
};
torrent?: any[];
[key: string]: any;
};
torrent?: unknown[];
[key: string]: unknown;
}
type ComicDetailProps = {
interface ComicDetailProps {
data: {
_id: string;
rawFileDetails?: RawFileDetailsType;
inferredMetadata: {
issue?: InferredIssue;
};
inferredMetadata: InferredMetadata;
sourcedMetadata: {
comicvine?: ComicVineMetadata;
locg?: any;
comicInfo?: any;
locg?: Record<string, unknown>;
comicInfo?: Record<string, unknown>;
};
acquisition?: Acquisition;
createdAt: string;
updatedAt: string;
};
userSettings?: any;
queryClient?: any;
userSettings?: Record<string, unknown>;
queryClient?: unknown;
comicObjectId?: string;
};
}
/**
* Displays full comic detail: cover, file info, action menu, and tabbed panels
* for metadata, archive operations, and acquisition.
*
* @param data.queryClient - react-query client passed through to the CV match
* panel so it can invalidate queries after a match is applied.
* @param data.comicObjectId - optional override for the comic ID; used when the
* component is rendered outside a route that provides the ID via `useParams`.
*/
export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const {
@@ -104,7 +99,8 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
setVisible(true);
}, []);
// Action menu handler
// Hide "match on Comic Vine" when there are no raw file details — matching
// requires file metadata to seed the search query.
const Placeholder = components.Placeholder;
const filteredActionOptions = filter(actionOptions, (item) => {
if (isUndefined(rawFileDetails)) {
@@ -180,6 +176,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
rawFileDetails={rawFileDetails}
inferredMetadata={inferredMetadata}
comicVineMatches={comicVineMatches}
// Prefer the route param; fall back to the data ID when rendered outside a route.
comicObjectId={comicObjectId || _id}
queryClient={queryClient}
onMatchApplied={() => {

View File

@@ -7,7 +7,7 @@ import TextareaAutosize from "react-textarea-autosize";
interface EditMetadataPanelProps {
data: {
name?: string;
name?: string | null;
[key: string]: any;
};
}

View File

@@ -2,27 +2,27 @@ import React from "react";
import { ComicVineSearchForm } from "./ComicVineSearchForm";
import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
import { EditMetadataPanel } from "./EditMetadataPanel";
import { RawFileDetails } from "../../graphql/generated";
import type { RawFileDetails, InferredMetadata } from "../../graphql/generated";
type InferredIssue = {
name?: string;
number?: number;
year?: string;
subtitle?: string;
[key: string]: any;
};
type CVMatchesPanelProps = {
interface CVMatchesPanelProps {
rawFileDetails?: RawFileDetails;
inferredMetadata: {
issue?: InferredIssue;
};
inferredMetadata: InferredMetadata;
comicVineMatches: any[];
comicObjectId: string;
queryClient: any;
onMatchApplied: () => void;
};
/**
* Sliding panel content for ComicVine match search.
*
* Renders a search form pre-populated from `rawFileDetails`, a preview of the
* inferred issue being searched for, and a list of ComicVine match candidates
* the user can apply to the comic.
*
* @param props.onMatchApplied - Called after the user selects and applies a match,
* allowing the parent to close the panel and refresh state.
*/
export const CVMatchesPanel: React.FC<CVMatchesPanelProps> = ({
rawFileDetails,
inferredMetadata,
@@ -62,4 +62,4 @@ type EditMetadataPanelWrapperProps = {
export const EditMetadataPanelWrapper: React.FC<EditMetadataPanelWrapperProps> = ({
rawFileDetails,
}) => <EditMetadataPanel data={rawFileDetails} />;
}) => <EditMetadataPanel data={rawFileDetails ?? {}} />;

View File

@@ -30,6 +30,7 @@ interface VolumeInformationProps {
onReconcile?: () => void;
}
/** Sources stored under `sourcedMetadata` — excludes `inferredMetadata`, which is checked separately. */
const SOURCED_METADATA_KEYS = ["comicvine", "locg", "comicInfo", "metron", "gcd"];
const SOURCE_LABELS: Record<string, string> = {
@@ -72,6 +73,17 @@ const MetadataSourceChips = ({
</div>
);
/**
* Displays volume metadata for a comic.
*
* - When multiple sources are present, renders a chip bar listing each source
* with a "Reconcile sources" action to merge them.
* - When exactly one source is present and it is ComicVine, renders the full
* ComicVine detail panel directly.
*
* @param props.data - Comic data containing sourced and inferred metadata.
* @param props.onReconcile - Called when the user triggers source reconciliation.
*/
export const VolumeInformation = (props: VolumeInformationProps): ReactElement => {
const { data, onReconcile } = props;
@@ -83,7 +95,7 @@ export const VolumeInformation = (props: VolumeInformationProps): ReactElement =
if (key === "locg") return Object.values(val as Record<string, unknown>).some((v) => !isNil(v) && v !== "");
return true;
});
if (!isNil(data?.inferredMetadata?.issue) && !isEmpty(data.inferredMetadata.issue)) {
if (!isNil(data?.inferredMetadata?.issue) && !isEmpty(data?.inferredMetadata?.issue)) {
sources.push("inferredMetadata");
}
return sources;