🏗️ Refactored the action menu

This commit is contained in:
2023-12-31 18:16:07 -05:00
parent b11cd76e37
commit 0ade3f9354
5 changed files with 252 additions and 257 deletions

View File

@@ -1,157 +1,13 @@
import { filter, isEmpty, isNil, isUndefined } from "lodash"; import React, { ReactElement } from "react";
import React, { ReactElement, useCallback, useState } from "react"; import Select from "react-select";
import Select, { components } from "react-select";
import { fetchComicVineMatches } from "../../../actions/fileops.actions";
import { refineQuery } from "filename-parser";
import { COMICVINE_SERVICE_URI } from "../../../constants/endpoints";
import axios from "axios";
export const Menu = (props): ReactElement => { export const Menu = (props): ReactElement => {
const { data } = props; const {
const { setSlidingPanelContentId, setVisible } = props.handlers; filteredActionOptions,
const [comicVineMatches, setComicVineMatches] = useState([]); customStyles,
handleActionSelection,
const fetchComicVineMatches = async ( Placeholder,
searchPayload, } = props.configuration;
issueSearchQuery,
seriesSearchQuery,
) => {
try {
await axios
.request({
url: `${COMICVINE_SERVICE_URI}/volumeBasedSearch`,
method: "POST",
data: {
format: "json",
// hack
query: issueSearchQuery.inferredIssueDetails.name
.replace(/[^a-zA-Z0-9 ]/g, "")
.trim(),
limit: "100",
page: 1,
resources: "volume",
scorerConfiguration: {
searchParams: issueSearchQuery.inferredIssueDetails,
},
rawFileDetails: searchPayload.rawFileDetails,
},
transformResponse: (r) => {
const matches = JSON.parse(r);
return matches;
// return sortBy(matches, (match) => -match.score);
},
})
.then((response) => {
let matches: any = [];
if (
!isNil(response.data.results) &&
response.data.results.length === 1
) {
matches = response.data.results;
} else {
matches = response.data.map((match) => match);
}
setComicVineMatches(matches);
});
} catch (err) {
console.log(err);
}
};
const openDrawerWithCVMatches = () => {
let seriesSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
let issueSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
if (!isUndefined(data.rawFileDetails)) {
issueSearchQuery = refineQuery(data.rawFileDetails.name);
} else if (!isEmpty(data.sourcedMetadata)) {
issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name);
}
fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery);
setSlidingPanelContentId("CVMatches");
setVisible(true);
};
const openEditMetadataPanel = useCallback(() => {
setSlidingPanelContentId("editComicBookMetadata");
setVisible(true);
}, []);
// Actions menu options and handler
const CVMatchLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--magic-stick-3-bold-duotone] w-6 h-6"></i>
</div>
<div>Match on ComicVine</div>
</span>
);
const editLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--pen-2-bold-duotone] w-6 h-6"></i>
</div>
<div>Edit Metadata</div>
</span>
);
const deleteLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--trash-bin-trash-bold-duotone] w-6 h-6"></i>
</div>
<div>Delete Comic</div>
</span>
);
const Placeholder = (props) => {
return <components.Placeholder {...props} />;
};
const actionOptions = [
{ value: "match-on-comic-vine", label: CVMatchLabel },
{ value: "edit-metdata", label: editLabel },
{ value: "delete-comic", label: deleteLabel },
];
const filteredActionOptions = filter(actionOptions, (item) => {
if (isUndefined(data.rawFileDetails)) {
return item.value !== "match-on-comic-vine";
}
return item;
});
const handleActionSelection = (action) => {
switch (action.value) {
case "match-on-comic-vine":
openDrawerWithCVMatches();
break;
case "edit-metdata":
openEditMetadataPanel();
break;
default:
console.log("No valid action selected.");
break;
}
};
const customStyles = {
menu: (base) => ({
...base,
backgroundColor: "rgb(156, 163, 175)",
}),
placeholder: (base) => ({
...base,
color: "black",
}),
option: (base, { data, isDisabled, isFocused, isSelected }) => ({
...base,
backgroundColor: isFocused ? "gray" : "rgb(156, 163, 175)",
}),
singleValue: (base) => ({
...base,
paddingTop: "0.4rem",
}),
control: (base) => ({
...base,
backgroundColor: "rgb(156, 163, 175)",
color: "black",
border: "1px solid rgb(156, 163, 175)",
}),
};
return ( return (
<Select <Select

View File

@@ -15,7 +15,8 @@ import AcquisitionPanel from "./AcquisitionPanel";
import DownloadsPanel from "./DownloadsPanel"; import DownloadsPanel from "./DownloadsPanel";
import { VolumeInformation } from "./Tabs/VolumeInformation"; import { VolumeInformation } from "./Tabs/VolumeInformation";
import { isEmpty, isUndefined, isNil } from "lodash"; import { isEmpty, isUndefined, isNil, filter } from "lodash";
import { components } from "react-select";
import { RootState } from "threetwo-ui-typings"; import { RootState } from "threetwo-ui-typings";
import "react-sliding-pane/dist/react-sliding-pane.css"; import "react-sliding-pane/dist/react-sliding-pane.css";
@@ -27,6 +28,9 @@ import ComicViewer from "react-comic-viewer";
import { extractComicArchive } from "../../actions/fileops.actions"; import { extractComicArchive } from "../../actions/fileops.actions";
import { determineCoverFile } from "../../shared/utils/metadata.utils"; import { determineCoverFile } from "../../shared/utils/metadata.utils";
import axios from "axios";
import { COMICVINE_SERVICE_URI } from "../../constants/endpoints";
import { refineQuery } from "filename-parser";
type ComicDetailProps = {}; type ComicDetailProps = {};
/** /**
@@ -56,6 +60,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [slidingPanelContentId, setSlidingPanelContentId] = useState(""); const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
const [modalIsOpen, setIsOpen] = useState(false); const [modalIsOpen, setIsOpen] = useState(false);
const [comicVineMatches, setComicVineMatches] = useState([]);
// const comicVineSearchResults = useSelector( // const comicVineSearchResults = useSelector(
// (state: RootState) => state.comicInfo.searchResults, // (state: RootState) => state.comicInfo.searchResults,
@@ -102,7 +107,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
CVMatches: { CVMatches: {
content: (props) => ( content: (props) => (
<> <>
{/* <div className="card search-criteria-card"> <div>
<div className="card-content"> <div className="card-content">
<ComicVineSearchForm data={rawFileDetails} /> <ComicVineSearchForm data={rawFileDetails} />
</div> </div>
@@ -114,29 +119,13 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
<span className="tag"> # {inferredMetadata.issue.number} </span> <span className="tag"> # {inferredMetadata.issue.number} </span>
</div> </div>
) : null} ) : null}
{!comicVineAPICallProgress ? (
<ComicVineMatchPanel <ComicVineMatchPanel
props={{ props={{
comicVineSearchQueryObject, comicVineMatches,
comicVineAPICallProgress, comicObjectId,
comicVineSearchResults, }}
comicObjectId, />
}}
/>
) : (
<div className="progress-indicator-container">
<div className="indicator">
<Loader
type="MutatingDots"
color="#CCC"
secondaryColor="#999"
height={100}
width={100}
visible={comicVineAPICallProgress}
/>
</div>
</div>
)} */}
</> </>
), ),
}, },
@@ -146,6 +135,154 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
}, },
}; };
// Actions
const fetchComicVineMatches = async (
searchPayload,
issueSearchQuery,
seriesSearchQuery,
) => {
try {
await axios
.request({
url: `${COMICVINE_SERVICE_URI}/volumeBasedSearch`,
method: "POST",
data: {
format: "json",
// hack
query: issueSearchQuery.inferredIssueDetails.name
.replace(/[^a-zA-Z0-9 ]/g, "")
.trim(),
limit: "100",
page: 1,
resources: "volume",
scorerConfiguration: {
searchParams: issueSearchQuery.inferredIssueDetails,
},
rawFileDetails: searchPayload.rawFileDetails,
},
transformResponse: (r) => {
const matches = JSON.parse(r);
return matches;
// return sortBy(matches, (match) => -match.score);
},
})
.then((response) => {
console.log(response);
let matches: any = [];
if (
!isNil(response.data.results) &&
response.data.results.length === 1
) {
matches = response.data.results;
} else {
matches = response.data.map((match) => match);
}
setComicVineMatches(matches);
});
} catch (err) {
console.log(err);
}
};
// Action event handlers
const openDrawerWithCVMatches = () => {
let seriesSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
let issueSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
if (!isUndefined(rawFileDetails)) {
issueSearchQuery = refineQuery(rawFileDetails.name);
} else if (!isEmpty(comicvine)) {
issueSearchQuery = refineQuery(comicvine.name);
}
fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery);
setSlidingPanelContentId("CVMatches");
setVisible(true);
};
const openEditMetadataPanel = useCallback(() => {
setSlidingPanelContentId("editComicBookMetadata");
setVisible(true);
}, []);
// Actions menu options and handler
const CVMatchLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--magic-stick-3-bold-duotone] w-6 h-6"></i>
</div>
<div>Match on ComicVine</div>
</span>
);
const editLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--pen-2-bold-duotone] w-6 h-6"></i>
</div>
<div>Edit Metadata</div>
</span>
);
const deleteLabel = (
<span className="inline-flex flex-row items-center gap-2">
<div className="w-6 h-6">
<i className="icon-[solar--trash-bin-trash-bold-duotone] w-6 h-6"></i>
</div>
<div>Delete Comic</div>
</span>
);
const Placeholder = (props) => {
return <components.Placeholder {...props} />;
};
const actionOptions = [
{ value: "match-on-comic-vine", label: CVMatchLabel },
{ value: "edit-metdata", label: editLabel },
{ value: "delete-comic", label: deleteLabel },
];
const filteredActionOptions = filter(actionOptions, (item) => {
if (isUndefined(rawFileDetails)) {
return item.value !== "match-on-comic-vine";
}
return item;
});
const handleActionSelection = (action) => {
switch (action.value) {
case "match-on-comic-vine":
openDrawerWithCVMatches();
break;
case "edit-metdata":
openEditMetadataPanel();
break;
default:
console.log("No valid action selected.");
break;
}
};
const customStyles = {
menu: (base) => ({
...base,
backgroundColor: "rgb(156, 163, 175)",
}),
placeholder: (base) => ({
...base,
color: "black",
}),
option: (base, { data, isDisabled, isFocused, isSelected }) => ({
...base,
backgroundColor: isFocused ? "gray" : "rgb(156, 163, 175)",
}),
singleValue: (base) => ({
...base,
paddingTop: "0.4rem",
}),
control: (base) => ({
...base,
backgroundColor: "rgb(156, 163, 175)",
color: "black",
border: "1px solid rgb(156, 163, 175)",
}),
};
// check for the availability of CV metadata // check for the availability of CV metadata
const isComicBookMetadataAvailable = const isComicBookMetadataAvailable =
!isUndefined(comicvine) && !isUndefined(comicvine.volumeInformation); !isUndefined(comicvine) && !isUndefined(comicvine.volumeInformation);
@@ -285,6 +422,12 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
<Menu <Menu
data={data.data} data={data.data}
handlers={{ setSlidingPanelContentId, setVisible }} handlers={{ setSlidingPanelContentId, setVisible }}
configuration={{
filteredActionOptions,
customStyles,
handleActionSelection,
Placeholder,
}}
/> />
</div> </div>
</RawFileDetails> </RawFileDetails>

View File

@@ -2,22 +2,26 @@ import React, { ReactElement } from "react";
import { ComicVineSearchForm } from "../ComicVineSearchForm"; import { ComicVineSearchForm } from "../ComicVineSearchForm";
import MatchResult from "./MatchResult"; import MatchResult from "./MatchResult";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow";
export const ComicVineMatchPanel = (comicVineData): ReactElement => { export const ComicVineMatchPanel = (comicVineData): ReactElement => {
const { const { comicObjectId, comicVineMatches } = comicVineData.props;
comicObjectId, const { comicvine } = useStore(
comicVineSearchQueryObject, useShallow((state) => ({
comicVineAPICallProgress, comicvine: state.comicvine,
comicVineSearchResults, })),
} = comicVineData.props; );
return ( return (
<> <>
<div className="search-results-container"> <div>
{!isEmpty(comicVineSearchResults) && ( {!isEmpty(comicVineMatches) ? (
<MatchResult <MatchResult
matchData={comicVineSearchResults} matchData={comicVineMatches}
comicObjectId={comicObjectId} comicObjectId={comicObjectId}
/> />
) : (
<>{comicvine.scrapingStatus}</>
)} )}
</div> </div>
</> </>

View File

@@ -1,8 +1,9 @@
import React, { useCallback } from "react"; import React from "react";
import { isNil, map } from "lodash"; import { isNil, map } from "lodash";
import { applyComicVineMatch } from "../../actions/comicinfo.actions";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
import axios from "axios";
interface MatchResultProps { interface MatchResultProps {
matchData: any; matchData: any;
@@ -14,12 +15,16 @@ const handleBrokenImage = (e) => {
}; };
export const MatchResult = (props: MatchResultProps) => { export const MatchResult = (props: MatchResultProps) => {
const applyCVMatch = useCallback( const applyCVMatch = async (match, comicObjectId) => {
// (match, comicObjectId) => { return await axios.request({
// dispatch(applyComicVineMatch(match, comicObjectId)); url: `${LIBRARY_SERVICE_BASE_URI}/applyComicVineMetadata`,
// }, method: "POST",
[], data: {
); match,
comicObjectId,
},
});
};
return ( return (
<> <>
{map(props.matchData, (match, idx) => { {map(props.matchData, (match, idx) => {
@@ -32,45 +37,29 @@ export const MatchResult = (props: MatchResultProps) => {
}); });
} }
return ( return (
<div className="search-result mb-4" key={idx}> <div className="mb-4" key={idx}>
<div className="columns"> <div className="flex flex-row">
<div className="column is-one-fifth"> <div className="w-full mr-2">
<img <img
className="cover-image" className="rounded-md"
src={match.image.thumb_url} src={match.image.thumb_url}
onError={handleBrokenImage} onError={handleBrokenImage}
/> />
</div> </div>
<div>
<div className="search-result-details column"> <div className="">{match.name}</div>
<div className="is-size-5">{match.name}</div> <span>Number</span>
<span>{match.issue_number}</span>
<div className="field is-grouped is-grouped-multiline mt-1"> <span className="tag">Cover Date</span>
<div className="control"> <span className="tag is-warning">{match.cover_date}</span>
<div className="tags has-addons"> <div className="text-sm">
<span className="tag">Number</span>
<span className="tag is-primary">
{match.issue_number}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">Cover Date</span>
<span className="tag is-warning">{match.cover_date}</span>
</div>
</div>
</div>
<div className="is-size-7">
{ellipsize(issueDescription, 300)} {ellipsize(issueDescription, 300)}
</div> </div>
</div> </div>
</div> </div>
<div className="vertical-line"></div> <div className="">
<div className="">
<div className="columns ml-6 volume-information">
<div className="column is-one-fifth">
<img <img
src={match.volumeInformation.results.image.icon_url} src={match.volumeInformation.results.image.icon_url}
className="cover-image" className="cover-image"
@@ -78,42 +67,30 @@ export const MatchResult = (props: MatchResultProps) => {
/> />
</div> </div>
<div className="column"> <div className="">
<div className="is-size-6">{match.volume.name}</div> <div className="">{match.volume.name}</div>
<div className="field is-grouped is-grouped-multiline mt-2"> <div className="">
<div className="control"> <span className="">Total Issues</span>
<div className="tags has-addons"> <span className="">
<span className="tag">Total Issues</span> {match.volumeInformation.results.count_of_issues}
<span className="tag is-warning"> </span>
{match.volumeInformation.results.count_of_issues}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">Publisher</span>
<span className="tag is-warning">
{match.volumeInformation.results.publisher.name}
</span>
</div>
</div>
</div> </div>
</div> </div>
<span className="tag">Publisher</span>
<span className="tag is-warning">
{match.volumeInformation.results.publisher.name}
</span>
</div> </div>
<div className="columns"> <button
<div className="column"> className="button is-normal is-outlined is-primary is-light is-pulled-right"
<button onClick={() => applyCVMatch(match, props.comicObjectId)}
className="button is-normal is-outlined is-primary is-light is-pulled-right" >
onClick={() => applyCVMatch(match, props.comicObjectId)} <span className="icon is-size-5">
> <i className="fas fa-clipboard-check"></i>
<span className="icon is-size-5"> </span>
<i className="fas fa-clipboard-check"></i> <span>Apply Match</span>
</span> </button>
<span>Apply Match</span>
</button>
</div>
</div>
</div> </div>
); );
})} })}

View File

@@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import { isEmpty, isNil, isUndefined } from "lodash"; import { isNil } from "lodash";
import io from "socket.io-client"; import io from "socket.io-client";
import { SOCKET_BASE_URI } from "../constants/endpoints"; import { SOCKET_BASE_URI } from "../constants/endpoints";
import { produce } from "immer"; import { produce } from "immer";
@@ -28,6 +28,11 @@ export const useStore = create((set, get) => ({
// Socket.io state // Socket.io state
socketIOInstance: {}, socketIOInstance: {},
// ComicVine Scraping status
comicvine: {
scrapingStatus: "",
},
// Import job queue and associated statuses // Import job queue and associated statuses
importJobQueue: { importJobQueue: {
successfulJobCount: 0, successfulJobCount: 0,
@@ -153,6 +158,16 @@ socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => {
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] }); queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
}); });
// ComicVine Scraping status
socketIOInstance.on("CV_SCRAPING_STATUS", (data) => {
setState((state) => ({
comicvine: {
...state.comicvine,
scrapingStatus: data.message,
},
}));
});
/** /**
* Method to init AirDC++ Socket with supplied settings * Method to init AirDC++ Socket with supplied settings
* @param configuration - credentials, and hostname details to init AirDC++ connection * @param configuration - credentials, and hostname details to init AirDC++ connection