Refactor scrape dialog (#4069)

* Fix performer select showing blank values after scrape
* Move and separate scrape dialog
* Separate row components from scene scrape dialog
* Refactor object creation
* Refactor gallery scrape dialog
This commit is contained in:
WithoutPants
2023-09-01 09:59:06 +10:00
committed by GitHub
parent 8abb3c0d08
commit fca162f1ca
12 changed files with 724 additions and 829 deletions

View File

@@ -1,292 +1,43 @@
import React, { useMemo, useState } from "react";
import React, { useState } from "react";
import * as GQL from "src/core/generated-graphql";
import {
MovieSelect,
TagSelect,
StudioSelect,
PerformerSelect,
} from "src/components/Shared/Select";
import {
ScrapeDialog,
ScrapeDialogRow,
ScrapeResult,
ScrapedInputGroupRow,
ScrapedTextAreaRow,
ScrapedImageRow,
IHasName,
ScrapedStringListRow,
} from "src/components/Shared/ScrapeDialog";
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
import clone from "lodash-es/clone";
import {
useStudioCreate,
usePerformerCreate,
useMovieCreate,
useTagCreate,
} from "src/core/StashService";
import { useToast } from "src/hooks/Toast";
import { useIntl } from "react-intl";
import { uniq } from "lodash-es";
import { scrapedPerformerToCreateInput } from "src/core/performers";
import { scrapedMovieToCreateInput } from "src/core/movies";
interface IScrapedStudioRow {
title: string;
result: ScrapeResult<string>;
onChange: (value: ScrapeResult<string>) => void;
newStudio?: GQL.ScrapedStudio;
onCreateNew?: (value: GQL.ScrapedStudio) => void;
}
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
title,
result,
onChange,
newStudio,
onCreateNew,
}) => {
function renderScrapedStudio(
scrapeResult: ScrapeResult<string>,
isNew?: boolean,
onChangeFn?: (value: string) => void
) {
const resultValue = isNew
? scrapeResult.newValue
: scrapeResult.originalValue;
const value = resultValue ? [resultValue] : [];
return (
<StudioSelect
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChangeFn) {
onChangeFn(items[0]?.id);
}
}}
ids={value}
/>
);
}
return (
<ScrapeDialogRow
title={title}
result={result}
renderOriginalField={() => renderScrapedStudio(result)}
renderNewField={() =>
renderScrapedStudio(result, true, (value) =>
onChange(result.cloneWithValue(value))
)
}
onChange={onChange}
newValues={newStudio ? [newStudio] : undefined}
onCreateNew={() => {
if (onCreateNew && newStudio) onCreateNew(newStudio);
}}
/>
);
};
interface IScrapedObjectsRow<T> {
title: string;
result: ScrapeResult<string[]>;
onChange: (value: ScrapeResult<string[]>) => void;
newObjects?: T[];
onCreateNew?: (value: T) => void;
renderObjects: (
result: ScrapeResult<string[]>,
isNew?: boolean,
onChange?: (value: string[]) => void
) => JSX.Element;
}
export const ScrapedObjectsRow = <T extends IHasName>(
props: IScrapedObjectsRow<T>
) => {
const { title, result, onChange, newObjects, onCreateNew, renderObjects } =
props;
return (
<ScrapeDialogRow
title={title}
result={result}
renderOriginalField={() => renderObjects(result)}
renderNewField={() =>
renderObjects(result, true, (value) =>
onChange(result.cloneWithValue(value))
)
}
onChange={onChange}
newValues={newObjects}
onCreateNew={(i) => {
if (onCreateNew) onCreateNew(newObjects![i]);
}}
/>
);
};
type IScrapedObjectRowImpl<T> = Omit<IScrapedObjectsRow<T>, "renderObjects">;
export const ScrapedPerformersRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedPerformer>
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
const performersCopy = useMemo(() => {
return (
newObjects?.map((p) => {
const name: string = p.name ?? "";
return { ...p, name };
}) ?? []
);
}, [newObjects]);
type PerformerType = GQL.ScrapedPerformer & {
name: string;
};
function renderScrapedPerformers(
scrapeResult: ScrapeResult<string[]>,
isNew?: boolean,
onChangeFn?: (value: string[]) => void
) {
const resultValue = isNew
? scrapeResult.newValue
: scrapeResult.originalValue;
const value = resultValue ?? [];
return (
<PerformerSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChangeFn) {
onChangeFn(items.map((i) => i.id));
}
}}
ids={value}
/>
);
}
return (
<ScrapedObjectsRow<PerformerType>
title={title}
result={result}
renderObjects={renderScrapedPerformers}
onChange={onChange}
newObjects={performersCopy}
onCreateNew={onCreateNew}
/>
);
};
export const ScrapedMoviesRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedMovie>
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
const moviesCopy = useMemo(() => {
return (
newObjects?.map((p) => {
const name: string = p.name ?? "";
return { ...p, name };
}) ?? []
);
}, [newObjects]);
type MovieType = GQL.ScrapedMovie & {
name: string;
};
function renderScrapedMovies(
scrapeResult: ScrapeResult<string[]>,
isNew?: boolean,
onChangeFn?: (value: string[]) => void
) {
const resultValue = isNew
? scrapeResult.newValue
: scrapeResult.originalValue;
const value = resultValue ?? [];
return (
<MovieSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChangeFn) {
onChangeFn(items.map((i) => i.id));
}
}}
ids={value}
/>
);
}
return (
<ScrapedObjectsRow<MovieType>
title={title}
result={result}
renderObjects={renderScrapedMovies}
onChange={onChange}
newObjects={moviesCopy}
onCreateNew={onCreateNew}
/>
);
};
export const ScrapedTagsRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedTag>
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
function renderScrapedTags(
scrapeResult: ScrapeResult<string[]>,
isNew?: boolean,
onChangeFn?: (value: string[]) => void
) {
const resultValue = isNew
? scrapeResult.newValue
: scrapeResult.originalValue;
const value = resultValue ?? [];
return (
<TagSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChangeFn) {
onChangeFn(items.map((i) => i.id));
}
}}
ids={value}
/>
);
}
return (
<ScrapedObjectsRow<GQL.ScrapedTag>
title={title}
result={result}
renderObjects={renderScrapedTags}
onChange={onChange}
newObjects={newObjects}
onCreateNew={onCreateNew}
/>
);
};
import { Performer } from "src/components/Performers/PerformerSelect";
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
import {
ScrapedMoviesRow,
ScrapedPerformersRow,
ScrapedStudioRow,
ScrapedTagsRow,
} from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
import {
useCreateScrapedMovie,
useCreateScrapedPerformer,
useCreateScrapedStudio,
useCreateScrapedTag,
} from "src/components/Shared/ScrapeDialog/createObjects";
interface ISceneScrapeDialogProps {
scene: Partial<GQL.SceneUpdateInput>;
scenePerformers: Performer[];
scraped: GQL.ScrapedScene;
endpoint?: string;
onClose: (scrapedScene?: GQL.ScrapedScene) => void;
}
interface IHasStoredID {
stored_id?: string | null;
}
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
scene,
scenePerformers,
scraped,
onClose,
endpoint,
@@ -365,10 +116,17 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
return ret;
}
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
new ScrapeResult<string[]>(
sortIdList(scene.performer_ids),
mapStoredIdObjects(scraped.performers ?? undefined)
const [performers, setPerformers] = useState<
ScrapeResult<GQL.ScrapedPerformer[]>
>(
new ScrapeResult<GQL.ScrapedPerformer[]>(
sortStoredIdObjects(
scenePerformers.map((p) => ({
stored_id: p.id,
name: p.name,
}))
),
sortStoredIdObjects(scraped.performers ?? undefined)
)
);
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
@@ -403,13 +161,34 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
new ScrapeResult<string>(scene.cover_image, scraped.image)
);
const [createStudio] = useStudioCreate();
const [createPerformer] = usePerformerCreate();
const [createMovie] = useMovieCreate();
const [createTag] = useTagCreate();
const createNewStudio = useCreateScrapedStudio({
scrapeResult: studio,
setScrapeResult: setStudio,
setNewObject: setNewStudio,
});
const createNewPerformer = useCreateScrapedPerformer({
scrapeResult: performers,
setScrapeResult: setPerformers,
newObjects: newPerformers,
setNewObjects: setNewPerformers,
});
const createNewMovie = useCreateScrapedMovie({
scrapeResult: movies,
setScrapeResult: setMovies,
newObjects: newMovies,
setNewObjects: setNewMovies,
});
const createNewTag = useCreateScrapedTag({
scrapeResult: tags,
setScrapeResult: setTags,
newObjects: newTags,
setNewObjects: setNewTags,
});
const intl = useIntl();
const Toast = useToast();
// don't show the dialog if nothing was scraped
if (
@@ -436,143 +215,6 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
return <></>;
}
async function createNewStudio(toCreate: GQL.ScrapedStudio) {
try {
const result = await createStudio({
variables: {
input: {
name: toCreate.name,
url: toCreate.url,
},
},
});
// set the new studio as the value
setStudio(studio.cloneWithValue(result.data!.studioCreate!.id));
setNewStudio(undefined);
Toast.success({
content: (
<span>
Created studio: <b>{toCreate.name}</b>
</span>
),
});
} catch (e) {
Toast.error(e);
}
}
async function createNewPerformer(toCreate: GQL.ScrapedPerformer) {
const input = scrapedPerformerToCreateInput(toCreate);
try {
const result = await createPerformer({
variables: { input },
});
const newValue = [...(performers.newValue ?? [])];
if (result.data?.performerCreate)
newValue.push(result.data.performerCreate.id);
// add the new performer to the new performers value
const performerClone = performers.cloneWithValue(newValue);
setPerformers(performerClone);
// remove the performer from the list
const newPerformersClone = newPerformers.concat();
const pIndex = newPerformersClone.findIndex(
(p) => p.name === toCreate.name
);
if (pIndex === -1) throw new Error("Could not find performer to remove");
newPerformersClone.splice(pIndex, 1);
setNewPerformers(newPerformersClone);
Toast.success({
content: (
<span>
Created performer: <b>{toCreate.name}</b>
</span>
),
});
} catch (e) {
Toast.error(e);
}
}
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
const movieInput = scrapedMovieToCreateInput(toCreate);
try {
const result = await createMovie({
variables: { input: movieInput },
});
// add the new movie to the new movies value
const movieClone = movies.cloneWithValue(movies.newValue);
if (!movieClone.newValue) {
movieClone.newValue = [];
}
movieClone.newValue.push(result.data!.movieCreate!.id);
setMovies(movieClone);
// remove the movie from the list
const newMoviesClone = newMovies.concat();
const pIndex = newMoviesClone.findIndex((p) => p.name === toCreate.name);
if (pIndex === -1) throw new Error("Could not find movie to remove");
newMoviesClone.splice(pIndex, 1);
setNewMovies(newMoviesClone);
Toast.success({
content: (
<span>
Created movie: <b>{toCreate.name}</b>
</span>
),
});
} catch (e) {
Toast.error(e);
}
}
async function createNewTag(toCreate: GQL.ScrapedTag) {
const tagInput: GQL.TagCreateInput = { name: toCreate.name ?? "" };
try {
const result = await createTag({
variables: {
input: tagInput,
},
});
const newValue = [...(tags.newValue ?? [])];
if (result.data?.tagCreate) newValue.push(result.data.tagCreate.id);
// add the new tag to the new tags value
const tagClone = tags.cloneWithValue(newValue);
setTags(tagClone);
// remove the tag from the list
const newTagsClone = newTags.concat();
const pIndex = newTagsClone.indexOf(toCreate);
if (pIndex === -1) throw new Error("Could not find tag to remove");
newTagsClone.splice(pIndex, 1);
setNewTags(newTagsClone);
Toast.success({
content: (
<span>
Created tag: <b>{toCreate.name}</b>
</span>
),
});
} catch (e) {
Toast.error(e);
}
}
function makeNewScrapedItem(): GQL.ScrapedSceneDataFragment {
const newStudioValue = studio.getNewValue();
@@ -588,12 +230,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
name: "",
}
: undefined,
performers: performers.getNewValue()?.map((p) => {
return {
stored_id: p,
name: "",
};
}),
performers: performers.getNewValue(),
movies: movies.getNewValue()?.map((m) => {
return {
stored_id: m,