🔨 Clearning up the VolumeInformation tab
This commit is contained in:
353
.claude/skills/typescript/SKILL.md
Normal file
353
.claude/skills/typescript/SKILL.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
---
|
||||||
|
name: typescript
|
||||||
|
description: TypeScript engineering guidelines based on Google's style guide. Use when writing, reviewing, or refactoring TypeScript code in this project.
|
||||||
|
---
|
||||||
|
|
||||||
|
Comprehensive guidelines for writing production-quality TypeScript based on Google's TypeScript Style Guide.
|
||||||
|
Naming Conventions
|
||||||
|
Type Convention Example
|
||||||
|
Classes, Interfaces, Types, Enums UpperCamelCase UserService, HttpClient
|
||||||
|
Variables, Parameters, Functions lowerCamelCase userName, processData
|
||||||
|
Global Constants, Enum Values CONSTANT_CASE MAX_RETRIES, Status.ACTIVE
|
||||||
|
Type Parameters Single letter or UpperCamelCase T, ResponseType
|
||||||
|
Naming Principles
|
||||||
|
|
||||||
|
Descriptive names, avoid ambiguous abbreviations
|
||||||
|
Treat acronyms as words: loadHttpUrl not loadHTTPURL
|
||||||
|
No prefixes like opt_ for optional parameters
|
||||||
|
No trailing underscores for private properties
|
||||||
|
Single-letter variables only when scope is <10 lines
|
||||||
|
|
||||||
|
Variable Declarations
|
||||||
|
|
||||||
|
// Always use const by default
|
||||||
|
const users = getUsers();
|
||||||
|
|
||||||
|
// Use let only when reassignment is needed
|
||||||
|
let count = 0;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Never use var
|
||||||
|
// var x = 1; // WRONG
|
||||||
|
|
||||||
|
// One variable per declaration
|
||||||
|
const a = 1;
|
||||||
|
const b = 2;
|
||||||
|
// const a = 1, b = 2; // WRONG
|
||||||
|
|
||||||
|
Types and Interfaces
|
||||||
|
Prefer Interfaces Over Type Aliases
|
||||||
|
|
||||||
|
// Good: interface for object shapes
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid: type alias for object shapes
|
||||||
|
type User = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type aliases OK for unions, intersections, mapped types
|
||||||
|
type Status = 'active' | 'inactive';
|
||||||
|
type Combined = TypeA & TypeB;
|
||||||
|
|
||||||
|
Type Inference
|
||||||
|
|
||||||
|
Leverage inference for trivially inferred types:
|
||||||
|
|
||||||
|
// Good: inference is clear
|
||||||
|
const name = 'Alice';
|
||||||
|
const items = [1, 2, 3];
|
||||||
|
|
||||||
|
// Good: explicit for complex expressions
|
||||||
|
const result: ProcessedData = complexTransformation(input);
|
||||||
|
|
||||||
|
Array Types
|
||||||
|
|
||||||
|
// Simple types: use T[]
|
||||||
|
const numbers: number[];
|
||||||
|
const names: readonly string[];
|
||||||
|
|
||||||
|
// Multi-dimensional: use T[][]
|
||||||
|
const matrix: number[][];
|
||||||
|
|
||||||
|
// Complex types: use Array<T>
|
||||||
|
const handlers: Array<(event: Event) => void>;
|
||||||
|
|
||||||
|
Null and Undefined
|
||||||
|
|
||||||
|
// Prefer optional fields over union with undefined
|
||||||
|
interface Config {
|
||||||
|
timeout?: number; // Good
|
||||||
|
// timeout: number | undefined; // Avoid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type aliases must NOT include |null or |undefined
|
||||||
|
type UserId = string; // Good
|
||||||
|
// type UserId = string | null; // WRONG
|
||||||
|
|
||||||
|
// May use == for null comparison (catches both null and undefined)
|
||||||
|
if (value == null) {
|
||||||
|
// handles both null and undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
Types to Avoid
|
||||||
|
|
||||||
|
// Avoid any - use unknown instead
|
||||||
|
function parse(input: unknown): Data { }
|
||||||
|
|
||||||
|
// Avoid {} - use unknown, Record<string, T>, or object
|
||||||
|
function process(obj: Record<string, unknown>): void { }
|
||||||
|
|
||||||
|
// Use lowercase primitives
|
||||||
|
let name: string; // Good
|
||||||
|
// let name: String; // WRONG
|
||||||
|
|
||||||
|
// Never use wrapper objects
|
||||||
|
// new String('hello') // WRONG
|
||||||
|
|
||||||
|
Classes
|
||||||
|
Structure
|
||||||
|
|
||||||
|
class UserService {
|
||||||
|
// Fields first, initialized where declared
|
||||||
|
private readonly cache = new Map<string, User>();
|
||||||
|
private lastAccess: Date | null = null;
|
||||||
|
|
||||||
|
// Constructor with parameter properties
|
||||||
|
constructor(
|
||||||
|
private readonly api: ApiClient,
|
||||||
|
private readonly logger: Logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Methods separated by blank lines
|
||||||
|
async getUser(id: string): Promise<User> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateId(id: string): boolean {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Visibility
|
||||||
|
|
||||||
|
class Example {
|
||||||
|
// private by default, only use public when needed externally
|
||||||
|
private internalState = 0;
|
||||||
|
|
||||||
|
// readonly for properties never reassigned after construction
|
||||||
|
readonly id: string;
|
||||||
|
|
||||||
|
// Never use #private syntax - use TypeScript visibility
|
||||||
|
// #field = 1; // WRONG
|
||||||
|
private field = 1; // Good
|
||||||
|
}
|
||||||
|
|
||||||
|
Avoid Arrow Functions as Properties
|
||||||
|
|
||||||
|
class Handler {
|
||||||
|
// Avoid: arrow function as property
|
||||||
|
// handleClick = () => { ... };
|
||||||
|
|
||||||
|
// Good: instance method
|
||||||
|
handleClick(): void {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind at call site if needed
|
||||||
|
element.addEventListener('click', () => handler.handleClick());
|
||||||
|
|
||||||
|
Static Methods
|
||||||
|
|
||||||
|
Never use this in static methods
|
||||||
|
Call on defining class, not subclasses
|
||||||
|
|
||||||
|
Functions
|
||||||
|
Prefer Function Declarations
|
||||||
|
|
||||||
|
// Good: function declaration for named functions
|
||||||
|
function processData(input: Data): Result {
|
||||||
|
return transform(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow functions when type annotation needed
|
||||||
|
const handler: EventHandler = (event) => {
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
Arrow Function Bodies
|
||||||
|
|
||||||
|
// Concise body only when return value is used
|
||||||
|
const double = (x: number) => x * 2;
|
||||||
|
|
||||||
|
// Block body when return should be void
|
||||||
|
const log = (msg: string) => {
|
||||||
|
console.log(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
|
||||||
|
// Use rest parameters, not arguments
|
||||||
|
function sum(...numbers: number[]): number {
|
||||||
|
return numbers.reduce((a, b) => a + b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructuring for multiple optional params
|
||||||
|
interface Options {
|
||||||
|
timeout?: number;
|
||||||
|
retries?: number;
|
||||||
|
}
|
||||||
|
function fetch(url: string, { timeout = 5000, retries = 3 }: Options = {}) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never name a parameter 'arguments'
|
||||||
|
|
||||||
|
Imports and Exports
|
||||||
|
Always Use Named Exports
|
||||||
|
|
||||||
|
// Good: named exports
|
||||||
|
export function processData() { }
|
||||||
|
export class UserService { }
|
||||||
|
export interface Config { }
|
||||||
|
|
||||||
|
// Never use default exports
|
||||||
|
// export default class UserService { } // WRONG
|
||||||
|
|
||||||
|
Import Styles
|
||||||
|
|
||||||
|
// Module import for large APIs
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
// Named imports for frequently used symbols
|
||||||
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
|
|
||||||
|
// Type-only imports when only used as types
|
||||||
|
import type { User, Config } from './types';
|
||||||
|
|
||||||
|
Module Organization
|
||||||
|
|
||||||
|
Use modules, never namespace Foo { }
|
||||||
|
Never use require() - use ES6 imports
|
||||||
|
Use relative imports within same project
|
||||||
|
Avoid excessive ../../../
|
||||||
|
|
||||||
|
Control Structures
|
||||||
|
Always Use Braces
|
||||||
|
|
||||||
|
// Good
|
||||||
|
if (condition) {
|
||||||
|
doSomething();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exception: single-line if
|
||||||
|
if (condition) return early;
|
||||||
|
|
||||||
|
Loops
|
||||||
|
|
||||||
|
// Prefer for...of for arrays
|
||||||
|
for (const item of items) {
|
||||||
|
process(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Object methods with for...of for objects
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never use unfiltered for...in on arrays
|
||||||
|
|
||||||
|
Equality
|
||||||
|
|
||||||
|
// Always use === and !==
|
||||||
|
if (a === b) { }
|
||||||
|
|
||||||
|
// Exception: == null catches both null and undefined
|
||||||
|
if (value == null) { }
|
||||||
|
|
||||||
|
Switch Statements
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case Status.Active:
|
||||||
|
handleActive();
|
||||||
|
break;
|
||||||
|
case Status.Inactive:
|
||||||
|
handleInactive();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Always include default, even if empty
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception Handling
|
||||||
|
|
||||||
|
// Always throw Error instances
|
||||||
|
throw new Error('Something went wrong');
|
||||||
|
// throw 'error'; // WRONG
|
||||||
|
|
||||||
|
// Catch with unknown type
|
||||||
|
try {
|
||||||
|
riskyOperation();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
logger.error(e.message);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty catch needs justification comment
|
||||||
|
try {
|
||||||
|
optional();
|
||||||
|
} catch {
|
||||||
|
// Intentionally ignored: fallback behavior handles this
|
||||||
|
}
|
||||||
|
|
||||||
|
Type Assertions
|
||||||
|
|
||||||
|
// Use 'as' syntax, not angle brackets
|
||||||
|
const input = value as string;
|
||||||
|
// const input = <string>value; // WRONG in TSX, avoid everywhere
|
||||||
|
|
||||||
|
// Double assertion through unknown when needed
|
||||||
|
const config = (rawData as unknown) as Config;
|
||||||
|
|
||||||
|
// Add comment explaining why assertion is safe
|
||||||
|
const element = document.getElementById('app') as HTMLElement;
|
||||||
|
// Safe: element exists in index.html
|
||||||
|
|
||||||
|
Strings
|
||||||
|
|
||||||
|
// Use single quotes for string literals
|
||||||
|
const name = 'Alice';
|
||||||
|
|
||||||
|
// Template literals for interpolation or multiline
|
||||||
|
const message = `Hello, ${name}!`;
|
||||||
|
const query = `
|
||||||
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Never use backslash line continuations
|
||||||
|
|
||||||
|
Disallowed Features
|
||||||
|
Feature Alternative
|
||||||
|
var const or let
|
||||||
|
Array() constructor [] literal
|
||||||
|
Object() constructor {} literal
|
||||||
|
any type unknown
|
||||||
|
namespace modules
|
||||||
|
require() import
|
||||||
|
Default exports Named exports
|
||||||
|
#private fields private modifier
|
||||||
|
eval() Never use
|
||||||
|
const enum Regular enum
|
||||||
|
debugger Remove before commit
|
||||||
|
with Never use
|
||||||
|
Prototype modification Never modify
|
||||||
@@ -130,6 +130,11 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
const isComicBookMetadataAvailable =
|
const isComicBookMetadataAvailable =
|
||||||
!isUndefined(comicvine) && !isUndefined(comicvine?.volumeInformation);
|
!isUndefined(comicvine) && !isUndefined(comicvine?.volumeInformation);
|
||||||
|
|
||||||
|
const hasAnyMetadata =
|
||||||
|
isComicBookMetadataAvailable ||
|
||||||
|
!isEmpty(comicInfo) ||
|
||||||
|
!isNil(locg);
|
||||||
|
|
||||||
const areRawFileDetailsAvailable =
|
const areRawFileDetailsAvailable =
|
||||||
!isUndefined(rawFileDetails) && !isEmpty(rawFileDetails);
|
!isUndefined(rawFileDetails) && !isEmpty(rawFileDetails);
|
||||||
|
|
||||||
@@ -147,16 +152,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create tab configuration
|
// Create tab configuration
|
||||||
|
const openReconcilePanel = useCallback(() => {
|
||||||
|
setSlidingPanelContentId("metadataReconciliation");
|
||||||
|
setVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const tabGroup = createTabConfig({
|
const tabGroup = createTabConfig({
|
||||||
data: data.data,
|
data: data.data,
|
||||||
comicInfo,
|
hasAnyMetadata,
|
||||||
isComicBookMetadataAvailable,
|
|
||||||
areRawFileDetailsAvailable,
|
areRawFileDetailsAvailable,
|
||||||
airDCPPQuery,
|
airDCPPQuery,
|
||||||
comicObjectId: _id,
|
comicObjectId: _id,
|
||||||
userSettings,
|
userSettings,
|
||||||
issueName,
|
issueName,
|
||||||
acquisition,
|
acquisition,
|
||||||
|
onReconcileMetadata: openReconcilePanel,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredTabs = tabGroup.filter((tab) => tab.shouldShow);
|
const filteredTabs = tabGroup.filter((tab) => tab.shouldShow);
|
||||||
|
|||||||
@@ -1,15 +1,108 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement, useMemo } from "react";
|
||||||
|
import { isEmpty, isNil } from "lodash";
|
||||||
import ComicVineDetails from "../ComicVineDetails";
|
import ComicVineDetails from "../ComicVineDetails";
|
||||||
|
|
||||||
export const VolumeInformation = (props): ReactElement => {
|
interface ComicVineMetadata {
|
||||||
const { data } = props;
|
volumeInformation?: Record<string, unknown>;
|
||||||
|
name?: string;
|
||||||
|
number?: string;
|
||||||
|
resource_type?: string;
|
||||||
|
id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SourcedMetadata {
|
||||||
|
comicvine?: ComicVineMetadata;
|
||||||
|
locg?: Record<string, unknown>;
|
||||||
|
comicInfo?: unknown;
|
||||||
|
metron?: unknown;
|
||||||
|
gcd?: unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VolumeInformationData {
|
||||||
|
sourcedMetadata?: SourcedMetadata;
|
||||||
|
inferredMetadata?: { issue?: unknown };
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VolumeInformationProps {
|
||||||
|
data: VolumeInformationData;
|
||||||
|
onReconcile?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SOURCED_METADATA_KEYS = ["comicvine", "locg", "comicInfo", "metron", "gcd"];
|
||||||
|
|
||||||
|
const SOURCE_LABELS: Record<string, string> = {
|
||||||
|
comicvine: "ComicVine",
|
||||||
|
locg: "League of Comic Geeks",
|
||||||
|
comicInfo: "ComicInfo.xml",
|
||||||
|
metron: "Metron",
|
||||||
|
gcd: "Grand Comics Database",
|
||||||
|
inferredMetadata: "Local File",
|
||||||
|
};
|
||||||
|
|
||||||
|
const MetadataSourceChips = ({
|
||||||
|
sources,
|
||||||
|
onReconcile,
|
||||||
|
}: {
|
||||||
|
sources: string[];
|
||||||
|
onReconcile: () => void;
|
||||||
|
}): ReactElement => (
|
||||||
|
<div className="flex flex-row flex-wrap items-center gap-2 mb-5 p-3 rounded-lg bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
|
||||||
|
<span className="text-xs text-slate-500 dark:text-slate-400 mr-1">
|
||||||
|
<i className="icon-[solar--database-outline] w-4 h-4 inline-block align-middle mr-1" />
|
||||||
|
{sources.length} metadata sources detected
|
||||||
|
</span>
|
||||||
|
{sources.map((source) => (
|
||||||
|
<span
|
||||||
|
key={source}
|
||||||
|
className="inline-flex items-center gap-1 bg-white dark:bg-slate-700 text-slate-700 dark:text-slate-300 text-xs font-medium px-2 py-1 rounded-md border border-slate-200 dark:border-slate-600"
|
||||||
|
>
|
||||||
|
<i className="icon-[solar--check-circle-outline] w-3 h-3" />
|
||||||
|
{SOURCE_LABELS[source] ?? source}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={onReconcile}
|
||||||
|
className="ml-auto inline-flex items-center gap-1 text-xs font-semibold text-sky-600 dark:text-sky-400 hover:text-sky-800 dark:hover:text-sky-200 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<i className="icon-[solar--refresh-outline] w-4 h-4" />
|
||||||
|
Reconcile sources
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const VolumeInformation = (props: VolumeInformationProps): ReactElement => {
|
||||||
|
const { data, onReconcile } = props;
|
||||||
|
|
||||||
|
const presentSources = useMemo(() => {
|
||||||
|
const sources = SOURCED_METADATA_KEYS.filter((key) => {
|
||||||
|
const val = (data?.sourcedMetadata ?? {})[key];
|
||||||
|
if (isNil(val) || isEmpty(val)) return false;
|
||||||
|
// locg returns an object even when empty; require at least one non-null value
|
||||||
|
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)) {
|
||||||
|
sources.push("inferredMetadata");
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}, [data?.sourcedMetadata, data?.inferredMetadata]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={1}>
|
<div key={1}>
|
||||||
<ComicVineDetails
|
{presentSources.length > 1 && (
|
||||||
data={data.sourcedMetadata.comicvine}
|
<MetadataSourceChips
|
||||||
updatedAt={data.updatedAt}
|
sources={presentSources}
|
||||||
/>
|
onReconcile={onReconcile ?? (() => {})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{presentSources.length === 1 && data.sourcedMetadata?.comicvine?.volumeInformation && (
|
||||||
|
<ComicVineDetails
|
||||||
|
data={data.sourcedMetadata.comicvine}
|
||||||
|
updatedAt={data.updatedAt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { lazy } from "react";
|
|||||||
import { isNil, isEmpty } from "lodash";
|
import { isNil, isEmpty } from "lodash";
|
||||||
|
|
||||||
const VolumeInformation = lazy(() => import("./Tabs/VolumeInformation").then(m => ({ default: m.VolumeInformation })));
|
const VolumeInformation = lazy(() => import("./Tabs/VolumeInformation").then(m => ({ default: m.VolumeInformation })));
|
||||||
const ComicInfoXML = lazy(() => import("./Tabs/ComicInfoXML").then(m => ({ default: m.ComicInfoXML })));
|
|
||||||
const ArchiveOperations = lazy(() => import("./Tabs/ArchiveOperations").then(m => ({ default: m.ArchiveOperations })));
|
const ArchiveOperations = lazy(() => import("./Tabs/ArchiveOperations").then(m => ({ default: m.ArchiveOperations })));
|
||||||
const AcquisitionPanel = lazy(() => import("./AcquisitionPanel"));
|
const AcquisitionPanel = lazy(() => import("./AcquisitionPanel"));
|
||||||
const TorrentSearchPanel = lazy(() => import("./TorrentSearchPanel"));
|
const TorrentSearchPanel = lazy(() => import("./TorrentSearchPanel"));
|
||||||
@@ -18,26 +17,26 @@ interface TabConfig {
|
|||||||
|
|
||||||
interface TabConfigParams {
|
interface TabConfigParams {
|
||||||
data: any;
|
data: any;
|
||||||
comicInfo: any;
|
hasAnyMetadata: boolean;
|
||||||
isComicBookMetadataAvailable: boolean;
|
|
||||||
areRawFileDetailsAvailable: boolean;
|
areRawFileDetailsAvailable: boolean;
|
||||||
airDCPPQuery: any;
|
airDCPPQuery: any;
|
||||||
comicObjectId: string;
|
comicObjectId: string;
|
||||||
userSettings: any;
|
userSettings: any;
|
||||||
issueName: string;
|
issueName: string;
|
||||||
acquisition?: any;
|
acquisition?: any;
|
||||||
|
onReconcileMetadata?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTabConfig = ({
|
export const createTabConfig = ({
|
||||||
data,
|
data,
|
||||||
comicInfo,
|
hasAnyMetadata,
|
||||||
isComicBookMetadataAvailable,
|
|
||||||
areRawFileDetailsAvailable,
|
areRawFileDetailsAvailable,
|
||||||
airDCPPQuery,
|
airDCPPQuery,
|
||||||
comicObjectId,
|
comicObjectId,
|
||||||
userSettings,
|
userSettings,
|
||||||
issueName,
|
issueName,
|
||||||
acquisition,
|
acquisition,
|
||||||
|
onReconcileMetadata,
|
||||||
}: TabConfigParams): TabConfig[] => {
|
}: TabConfigParams): TabConfig[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -46,23 +45,10 @@ export const createTabConfig = ({
|
|||||||
icon: (
|
icon: (
|
||||||
<i className="h-5 w-5 icon-[solar--book-2-bold] text-slate-500 dark:text-slate-300"></i>
|
<i className="h-5 w-5 icon-[solar--book-2-bold] text-slate-500 dark:text-slate-300"></i>
|
||||||
),
|
),
|
||||||
content: isComicBookMetadataAvailable ? (
|
content: hasAnyMetadata ? (
|
||||||
<VolumeInformation data={data} key={1} />
|
<VolumeInformation data={data} onReconcile={onReconcileMetadata} key={1} />
|
||||||
) : null,
|
) : null,
|
||||||
shouldShow: isComicBookMetadataAvailable,
|
shouldShow: hasAnyMetadata,
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "ComicInfo.xml",
|
|
||||||
icon: (
|
|
||||||
<i className="h-5 w-5 icon-[solar--code-file-bold-duotone] text-slate-500 dark:text-slate-300" />
|
|
||||||
),
|
|
||||||
content: (
|
|
||||||
<div key={2}>
|
|
||||||
{!isNil(comicInfo) && <ComicInfoXML json={comicInfo} />}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
shouldShow: !isEmpty(comicInfo),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
|
|||||||
Reference in New Issue
Block a user