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

@@ -255,6 +255,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
return ( return (
<GalleryScrapeDialog <GalleryScrapeDialog
gallery={currentGallery} gallery={currentGallery}
galleryPerformers={performers}
scraped={scrapedGallery} scraped={scrapedGallery}
onClose={(data) => { onClose={(data) => {
onScrapeDialogClosed(data); onScrapeDialogClosed(data);

View File

@@ -1,180 +1,29 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { useIntl } from "react-intl";
import {
StudioSelect,
PerformerSelect,
TagSelect,
} from "src/components/Shared/Select";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
ScrapeDialog, ScrapeDialog,
ScrapeDialogRow,
ScrapeResult,
ScrapedInputGroupRow, ScrapedInputGroupRow,
ScrapedTextAreaRow, ScrapedTextAreaRow,
} from "src/components/Shared/ScrapeDialog"; } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
import clone from "lodash-es/clone"; import clone from "lodash-es/clone";
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
import { import {
useStudioCreate, ScrapedPerformersRow,
usePerformerCreate, ScrapedStudioRow,
useTagCreate, ScrapedTagsRow,
} from "src/core/StashService"; } from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
import { useToast } from "src/hooks/Toast"; import { sortStoredIdObjects } from "src/utils/data";
import { scrapedPerformerToCreateInput } from "src/core/performers"; import { Performer } from "src/components/Performers/PerformerSelect";
import {
function renderScrapedStudio( useCreateScrapedPerformer,
result: ScrapeResult<string>, useCreateScrapedStudio,
isNew?: boolean, useCreateScrapedTag,
onChange?: (value: string) => void } from "src/components/Shared/ScrapeDialog/createObjects";
) {
const resultValue = isNew ? result.newValue : result.originalValue;
const value = resultValue ? [resultValue] : [];
return (
<StudioSelect
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChange) {
onChange(items[0]?.id);
}
}}
ids={value}
/>
);
}
function renderScrapedStudioRow(
title: string,
result: ScrapeResult<string>,
onChange: (value: ScrapeResult<string>) => void,
newStudio?: GQL.ScrapedStudio,
onCreateNew?: (value: GQL.ScrapedStudio) => void
) {
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);
}}
/>
);
}
function renderScrapedPerformers(
result: ScrapeResult<string[]>,
isNew?: boolean,
onChange?: (value: string[]) => void
) {
const resultValue = isNew ? result.newValue : result.originalValue;
const value = resultValue ?? [];
return (
<PerformerSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChange) {
onChange(items.map((i) => i.id));
}
}}
ids={value}
/>
);
}
function renderScrapedPerformersRow(
title: string,
result: ScrapeResult<string[]>,
onChange: (value: ScrapeResult<string[]>) => void,
newPerformers: GQL.ScrapedPerformer[],
onCreateNew?: (value: GQL.ScrapedPerformer) => void
) {
const performersCopy = newPerformers.map((p) => {
const name: string = p.name ?? "";
return { ...p, name };
});
return (
<ScrapeDialogRow
title={title}
result={result}
renderOriginalField={() => renderScrapedPerformers(result)}
renderNewField={() =>
renderScrapedPerformers(result, true, (value) =>
onChange(result.cloneWithValue(value))
)
}
onChange={onChange}
newValues={performersCopy}
onCreateNew={(i) => {
if (onCreateNew) onCreateNew(newPerformers[i]);
}}
/>
);
}
function renderScrapedTags(
result: ScrapeResult<string[]>,
isNew?: boolean,
onChange?: (value: string[]) => void
) {
const resultValue = isNew ? result.newValue : result.originalValue;
const value = resultValue ?? [];
return (
<TagSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChange) {
onChange(items.map((i) => i.id));
}
}}
ids={value}
/>
);
}
function renderScrapedTagsRow(
title: string,
result: ScrapeResult<string[]>,
onChange: (value: ScrapeResult<string[]>) => void,
newTags: GQL.ScrapedTag[],
onCreateNew?: (value: GQL.ScrapedTag) => void
) {
return (
<ScrapeDialogRow
title={title}
result={result}
renderOriginalField={() => renderScrapedTags(result)}
renderNewField={() =>
renderScrapedTags(result, true, (value) =>
onChange(result.cloneWithValue(value))
)
}
newValues={newTags}
onChange={onChange}
onCreateNew={(i) => {
if (onCreateNew) onCreateNew(newTags[i]);
}}
/>
);
}
interface IGalleryScrapeDialogProps { interface IGalleryScrapeDialogProps {
gallery: Partial<GQL.GalleryUpdateInput>; gallery: Partial<GQL.GalleryUpdateInput>;
galleryPerformers: Performer[];
scraped: GQL.ScrapedGallery; scraped: GQL.ScrapedGallery;
onClose: (scrapedGallery?: GQL.ScrapedGallery) => void; onClose: (scrapedGallery?: GQL.ScrapedGallery) => void;
@@ -247,10 +96,17 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
return ret; return ret;
} }
const [performers, setPerformers] = useState<ScrapeResult<string[]>>( const [performers, setPerformers] = useState<
new ScrapeResult<string[]>( ScrapeResult<GQL.ScrapedPerformer[]>
sortIdList(props.gallery.performer_ids), >(
mapStoredIdObjects(props.scraped.performers ?? undefined) new ScrapeResult<GQL.ScrapedPerformer[]>(
sortStoredIdObjects(
props.galleryPerformers.map((p) => ({
stored_id: p.id,
name: p.name,
}))
),
sortStoredIdObjects(props.scraped.performers ?? undefined)
) )
); );
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>( const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
@@ -271,11 +127,25 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
new ScrapeResult<string>(props.gallery.details, props.scraped.details) new ScrapeResult<string>(props.gallery.details, props.scraped.details)
); );
const [createStudio] = useStudioCreate(); const createNewStudio = useCreateScrapedStudio({
const [createPerformer] = usePerformerCreate(); scrapeResult: studio,
const [createTag] = useTagCreate(); setScrapeResult: setStudio,
setNewObject: setNewStudio,
});
const Toast = useToast(); const createNewPerformer = useCreateScrapedPerformer({
scrapeResult: performers,
setScrapeResult: setPerformers,
newObjects: newPerformers,
setNewObjects: setNewPerformers,
});
const createNewTag = useCreateScrapedTag({
scrapeResult: tags,
setScrapeResult: setTags,
newObjects: newTags,
setNewObjects: setNewTags,
});
// don't show the dialog if nothing was scraped // don't show the dialog if nothing was scraped
if ( if (
@@ -290,122 +160,6 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
return <></>; 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>
<FormattedMessage
id="actions.created_entity"
values={{
entity_type: intl.formatMessage({ id: "studio" }),
entity_name: <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 },
});
// add the new performer to the new performers value
const performerClone = performers.cloneWithValue(performers.newValue);
if (!performerClone.newValue) {
performerClone.newValue = [];
}
performerClone.newValue.push(result.data!.performerCreate!.id);
setPerformers(performerClone);
// remove the performer from the list
const newPerformersClone = newPerformers.concat();
const pIndex = newPerformersClone.indexOf(toCreate);
newPerformersClone.splice(pIndex, 1);
setNewPerformers(newPerformersClone);
Toast.success({
content: (
<span>
<FormattedMessage
id="actions.created_entity"
values={{
entity_type: intl.formatMessage({ id: "performer" }),
entity_name: <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,
},
});
// add the new tag to the new tags value
const tagClone = tags.cloneWithValue(tags.newValue);
if (!tagClone.newValue) {
tagClone.newValue = [];
}
tagClone.newValue.push(result.data!.tagCreate!.id);
setTags(tagClone);
// remove the tag from the list
const newTagsClone = newTags.concat();
const pIndex = newTagsClone.indexOf(toCreate);
newTagsClone.splice(pIndex, 1);
setNewTags(newTagsClone);
Toast.success({
content: (
<span>
<FormattedMessage
id="actions.created_entity"
values={{
entity_type: intl.formatMessage({ id: "tag" }),
entity_name: <b>{toCreate.name}</b>,
}}
/>
</span>
),
});
} catch (e) {
Toast.error(e);
}
}
function makeNewScrapedItem(): GQL.ScrapedGalleryDataFragment { function makeNewScrapedItem(): GQL.ScrapedGalleryDataFragment {
const newStudioValue = studio.getNewValue(); const newStudioValue = studio.getNewValue();
@@ -419,12 +173,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
name: "", name: "",
} }
: undefined, : undefined,
performers: performers.getNewValue()?.map((p) => { performers: performers.getNewValue(),
return {
stored_id: p,
name: "",
};
}),
tags: tags.getNewValue()?.map((m) => { tags: tags.getNewValue()?.map((m) => {
return { return {
stored_id: m, stored_id: m,
@@ -454,27 +203,27 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
result={date} result={date}
onChange={(value) => setDate(value)} onChange={(value) => setDate(value)}
/> />
{renderScrapedStudioRow( <ScrapedStudioRow
intl.formatMessage({ id: "studios" }), title={intl.formatMessage({ id: "studios" })}
studio, result={studio}
(value) => setStudio(value), onChange={(value) => setStudio(value)}
newStudio, newStudio={newStudio}
createNewStudio onCreateNew={createNewStudio}
)} />
{renderScrapedPerformersRow( <ScrapedPerformersRow
intl.formatMessage({ id: "performers" }), title={intl.formatMessage({ id: "performers" })}
performers, result={performers}
(value) => setPerformers(value), onChange={(value) => setPerformers(value)}
newPerformers, newObjects={newPerformers}
createNewPerformer onCreateNew={createNewPerformer}
)} />
{renderScrapedTagsRow( <ScrapedTagsRow
intl.formatMessage({ id: "tags" }), title={intl.formatMessage({ id: "tags" })}
tags, result={tags}
(value) => setTags(value), onChange={(value) => setTags(value)}
newTags, newObjects={newTags}
createNewTag onCreateNew={createNewTag}
)} />
<ScrapedTextAreaRow <ScrapedTextAreaRow
title={intl.formatMessage({ id: "details" })} title={intl.formatMessage({ id: "details" })}
result={details} result={details}

View File

@@ -3,16 +3,16 @@ import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
ScrapeDialog, ScrapeDialog,
ScrapeResult,
ScrapedInputGroupRow, ScrapedInputGroupRow,
ScrapedImageRow, ScrapedImageRow,
ScrapeDialogRow, ScrapeDialogRow,
ScrapedTextAreaRow, ScrapedTextAreaRow,
} from "src/components/Shared/ScrapeDialog"; } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
import { StudioSelect } from "src/components/Shared/Select"; import { StudioSelect } from "src/components/Shared/Select";
import DurationUtils from "src/utils/duration"; import DurationUtils from "src/utils/duration";
import { useStudioCreate } from "src/core/StashService"; import { useStudioCreate } from "src/core/StashService";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
function renderScrapedStudio( function renderScrapedStudio(
result: ScrapeResult<string>, result: ScrapeResult<string>,

View File

@@ -3,13 +3,12 @@ import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
ScrapeDialog, ScrapeDialog,
ScrapeResult,
ScrapedInputGroupRow, ScrapedInputGroupRow,
ScrapedImagesRow, ScrapedImagesRow,
ScrapeDialogRow, ScrapeDialogRow,
ScrapedTextAreaRow, ScrapedTextAreaRow,
ScrapedCountryRow, ScrapedCountryRow,
} from "src/components/Shared/ScrapeDialog"; } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
import { useTagCreate } from "src/core/StashService"; import { useTagCreate } from "src/core/StashService";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
import { TagSelect } from "src/components/Shared/Select"; import { TagSelect } from "src/components/Shared/Select";
@@ -26,6 +25,7 @@ import {
stringToCircumcised, stringToCircumcised,
} from "src/utils/circumcised"; } from "src/utils/circumcised";
import { IStashBox } from "./PerformerStashBoxModal"; import { IStashBox } from "./PerformerStashBoxModal";
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
function renderScrapedGender( function renderScrapedGender(
result: ScrapeResult<string>, result: ScrapeResult<string>,

View File

@@ -430,6 +430,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
return ( return (
<SceneScrapeDialog <SceneScrapeDialog
scene={currentScene} scene={currentScene}
scenePerformers={performers}
scraped={scrapedScene} scraped={scrapedScene}
endpoint={endpoint} endpoint={endpoint}
onClose={(s) => onScrapeDialogClosed(s)} onClose={(s) => onScrapeDialogClosed(s)}

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 * as GQL from "src/core/generated-graphql";
import {
MovieSelect,
TagSelect,
StudioSelect,
PerformerSelect,
} from "src/components/Shared/Select";
import { import {
ScrapeDialog, ScrapeDialog,
ScrapeDialogRow,
ScrapeResult,
ScrapedInputGroupRow, ScrapedInputGroupRow,
ScrapedTextAreaRow, ScrapedTextAreaRow,
ScrapedImageRow, ScrapedImageRow,
IHasName,
ScrapedStringListRow, ScrapedStringListRow,
} from "src/components/Shared/ScrapeDialog"; } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
import clone from "lodash-es/clone"; 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 { useIntl } from "react-intl";
import { uniq } from "lodash-es"; import { uniq } from "lodash-es";
import { scrapedPerformerToCreateInput } from "src/core/performers"; import { Performer } from "src/components/Performers/PerformerSelect";
import { scrapedMovieToCreateInput } from "src/core/movies"; import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
interface IScrapedStudioRow { import {
title: string; ScrapedMoviesRow,
result: ScrapeResult<string>; ScrapedPerformersRow,
onChange: (value: ScrapeResult<string>) => void; ScrapedStudioRow,
newStudio?: GQL.ScrapedStudio; ScrapedTagsRow,
onCreateNew?: (value: GQL.ScrapedStudio) => void; } from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
} import {
useCreateScrapedMovie,
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({ useCreateScrapedPerformer,
title, useCreateScrapedStudio,
result, useCreateScrapedTag,
onChange, } from "src/components/Shared/ScrapeDialog/createObjects";
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}
/>
);
};
interface ISceneScrapeDialogProps { interface ISceneScrapeDialogProps {
scene: Partial<GQL.SceneUpdateInput>; scene: Partial<GQL.SceneUpdateInput>;
scenePerformers: Performer[];
scraped: GQL.ScrapedScene; scraped: GQL.ScrapedScene;
endpoint?: string; endpoint?: string;
onClose: (scrapedScene?: GQL.ScrapedScene) => void; onClose: (scrapedScene?: GQL.ScrapedScene) => void;
} }
interface IHasStoredID {
stored_id?: string | null;
}
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
scene, scene,
scenePerformers,
scraped, scraped,
onClose, onClose,
endpoint, endpoint,
@@ -365,10 +116,17 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
return ret; return ret;
} }
const [performers, setPerformers] = useState<ScrapeResult<string[]>>( const [performers, setPerformers] = useState<
new ScrapeResult<string[]>( ScrapeResult<GQL.ScrapedPerformer[]>
sortIdList(scene.performer_ids), >(
mapStoredIdObjects(scraped.performers ?? undefined) 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[]>( const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
@@ -403,13 +161,34 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
new ScrapeResult<string>(scene.cover_image, scraped.image) new ScrapeResult<string>(scene.cover_image, scraped.image)
); );
const [createStudio] = useStudioCreate(); const createNewStudio = useCreateScrapedStudio({
const [createPerformer] = usePerformerCreate(); scrapeResult: studio,
const [createMovie] = useMovieCreate(); setScrapeResult: setStudio,
const [createTag] = useTagCreate(); 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 intl = useIntl();
const Toast = useToast();
// don't show the dialog if nothing was scraped // don't show the dialog if nothing was scraped
if ( if (
@@ -436,143 +215,6 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
return <></>; 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 { function makeNewScrapedItem(): GQL.ScrapedSceneDataFragment {
const newStudioValue = studio.getNewValue(); const newStudioValue = studio.getNewValue();
@@ -588,12 +230,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
name: "", name: "",
} }
: undefined, : undefined,
performers: performers.getNewValue()?.map((p) => { performers: performers.getNewValue(),
return {
stored_id: p,
name: "",
};
}),
movies: movies.getNewValue()?.map((m) => { movies: movies.getNewValue()?.map((m) => {
return { return {
stored_id: m, stored_id: m,

View File

@@ -12,26 +12,29 @@ import { FormattedMessage, useIntl } from "react-intl";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons"; import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
import { import {
hasScrapedValues,
ScrapeDialog, ScrapeDialog,
ScrapeDialogRow, ScrapeDialogRow,
ScrapedImageRow, ScrapedImageRow,
ScrapedInputGroupRow, ScrapedInputGroupRow,
ScrapedStringListRow, ScrapedStringListRow,
ScrapedTextAreaRow, ScrapedTextAreaRow,
} from "../Shared/ScrapeDialog/ScrapeDialog";
import { clone, uniq } from "lodash-es";
import { galleryTitle } from "src/core/galleries";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { ModalComponent } from "../Shared/Modal";
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
import {
ScrapeResult, ScrapeResult,
ZeroableScrapeResult, ZeroableScrapeResult,
} from "../Shared/ScrapeDialog"; hasScrapedValues,
import { clone, uniq } from "lodash-es"; } from "../Shared/ScrapeDialog/scrapeResult";
import { import {
ScrapedMoviesRow, ScrapedMoviesRow,
ScrapedPerformersRow, ScrapedPerformersRow,
ScrapedStudioRow, ScrapedStudioRow,
ScrapedTagsRow, ScrapedTagsRow,
} from "./SceneDetails/SceneScrapeDialog"; } from "../Shared/ScrapeDialog/ScrapedObjectsRow";
import { galleryTitle } from "src/core/galleries";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { ModalComponent } from "../Shared/Modal";
interface IStashIDsField { interface IStashIDsField {
values: GQL.StashId[]; values: GQL.StashId[];
@@ -101,8 +104,25 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
return ret; return ret;
} }
const [performers, setPerformers] = useState<ScrapeResult<string[]>>( function idToStoredID(o: { id: string; name: string }) {
new ScrapeResult<string[]>(sortIdList(dest.performers.map((p) => p.id))) return {
stored_id: o.id,
name: o.name,
};
}
function uniqIDStoredIDs(objs: IHasStoredID[]) {
return objs.filter((o, i) => {
return objs.findIndex((oo) => oo.stored_id === o.stored_id) === i;
});
}
const [performers, setPerformers] = useState<
ScrapeResult<GQL.ScrapedPerformer[]>
>(
new ScrapeResult<GQL.ScrapedPerformer[]>(
sortStoredIdObjects(dest.performers.map(idToStoredID))
)
); );
const [movies, setMovies] = useState<ScrapeResult<string[]>>( const [movies, setMovies] = useState<ScrapeResult<string[]>>(
@@ -184,8 +204,8 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
setPerformers( setPerformers(
new ScrapeResult( new ScrapeResult(
dest.performers.map((p) => p.id), dest.performers.map(idToStoredID),
uniq(all.map((s) => s.performers.map((p) => p.id)).flat()) uniqIDStoredIDs(all.map((s) => s.performers.map(idToStoredID)).flat())
) )
); );
setTags( setTags(
@@ -559,7 +579,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
play_duration: playDuration.getNewValue(), play_duration: playDuration.getNewValue(),
gallery_ids: galleries.getNewValue(), gallery_ids: galleries.getNewValue(),
studio_id: studio.getNewValue(), studio_id: studio.getNewValue(),
performer_ids: performers.getNewValue(), performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
movies: movies.getNewValue()?.map((m) => { movies: movies.getNewValue()?.map((m) => {
// find the equivalent movie in the original scenes // find the equivalent movie in the original scenes
const found = all const found = all

View File

@@ -8,10 +8,9 @@ import {
FormControl, FormControl,
Badge, Badge,
} from "react-bootstrap"; } from "react-bootstrap";
import { CollapseButton } from "./CollapseButton"; import { CollapseButton } from "../CollapseButton";
import { Icon } from "./Icon"; import { Icon } from "../Icon";
import { ModalComponent } from "./Modal"; import { ModalComponent } from "../Modal";
import isEqual from "lodash-es/isEqual";
import clone from "lodash-es/clone"; import clone from "lodash-es/clone";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { import {
@@ -21,78 +20,10 @@ import {
faTimes, faTimes,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { getCountryByISO } from "src/utils/country"; import { getCountryByISO } from "src/utils/country";
import { CountrySelect } from "./CountrySelect"; import { CountrySelect } from "../CountrySelect";
import { StringListInput } from "./StringListInput"; import { StringListInput } from "../StringListInput";
import { ImageSelector } from "./ImageSelector"; import { ImageSelector } from "../ImageSelector";
import { ScrapeResult } from "./scrapeResult";
export class ScrapeResult<T> {
public newValue?: T;
public originalValue?: T;
public scraped: boolean = false;
public useNewValue: boolean = false;
public constructor(
originalValue?: T | null,
newValue?: T | null,
useNewValue?: boolean
) {
this.originalValue = originalValue ?? undefined;
this.newValue = newValue ?? undefined;
// NOTE: this means that zero values are treated as null
// this is incorrect for numbers and booleans, but correct for strings
const hasNewValue = !!this.newValue;
const valuesEqual = isEqual(originalValue, newValue);
this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual);
this.scraped = hasNewValue && !valuesEqual;
}
public setOriginalValue(value?: T) {
this.originalValue = value;
this.newValue = value;
}
public cloneWithValue(value?: T) {
const ret = clone(this);
ret.newValue = value;
ret.useNewValue = !isEqual(ret.newValue, ret.originalValue);
// #2691 - if we're setting the value, assume it should be treated as
// scraped
ret.scraped = true;
return ret;
}
public getNewValue() {
if (this.useNewValue) {
return this.newValue;
}
}
}
// for types where !!value is a valid value (boolean and number)
export class ZeroableScrapeResult<T> extends ScrapeResult<T> {
public constructor(
originalValue?: T | null,
newValue?: T | null,
useNewValue?: boolean
) {
super(originalValue, newValue, useNewValue);
const hasNewValue = this.newValue !== undefined;
const valuesEqual = isEqual(originalValue, newValue);
this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual);
this.scraped = hasNewValue && !valuesEqual;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function hasScrapedValues(values: ScrapeResult<any>[]) {
return values.some((r) => r.scraped);
}
export interface IHasName { export interface IHasName {
name: string | undefined; name: string | undefined;

View File

@@ -0,0 +1,269 @@
import React, { useMemo } from "react";
import * as GQL from "src/core/generated-graphql";
import {
MovieSelect,
TagSelect,
StudioSelect,
} from "src/components/Shared/Select";
import {
ScrapeDialogRow,
IHasName,
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
import { PerformerSelect } from "src/components/Performers/PerformerSelect";
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
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, R> {
title: string;
result: ScrapeResult<R[]>;
onChange: (value: ScrapeResult<R[]>) => void;
newObjects?: T[];
onCreateNew?: (value: T) => void;
renderObjects: (
result: ScrapeResult<R[]>,
isNew?: boolean,
onChange?: (value: R[]) => void
) => JSX.Element;
}
export const ScrapedObjectsRow = <T extends IHasName, R>(
props: IScrapedObjectsRow<T, R>
) => {
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, R> = Omit<
IScrapedObjectsRow<T, R>,
"renderObjects"
>;
export const ScrapedPerformersRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedPerformer, GQL.ScrapedPerformer>
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
const performersCopy = useMemo(() => {
return (
newObjects?.map((p) => {
const name: string = p.name ?? "";
return { ...p, name };
}) ?? []
);
}, [newObjects]);
function renderScrapedPerformers(
scrapeResult: ScrapeResult<GQL.ScrapedPerformer[]>,
isNew?: boolean,
onChangeFn?: (value: GQL.ScrapedPerformer[]) => void
) {
const resultValue = isNew
? scrapeResult.newValue
: scrapeResult.originalValue;
const value = resultValue ?? [];
const selectValue = value.map((p) => {
const alias_list: string[] = [];
return {
id: p.stored_id ?? "",
name: p.name ?? "",
alias_list,
};
});
return (
<PerformerSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChangeFn) {
onChangeFn(items);
}
}}
values={selectValue}
/>
);
}
type PerformerType = GQL.ScrapedPerformer & {
name: string;
};
return (
<ScrapedObjectsRow<PerformerType, GQL.ScrapedPerformer>
title={title}
result={result}
renderObjects={renderScrapedPerformers}
onChange={onChange}
newObjects={performersCopy}
onCreateNew={onCreateNew}
/>
);
};
export const ScrapedMoviesRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedMovie, string>
> = ({ 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, string>
title={title}
result={result}
renderObjects={renderScrapedMovies}
onChange={onChange}
newObjects={moviesCopy}
onCreateNew={onCreateNew}
/>
);
};
export const ScrapedTagsRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedTag, string>
> = ({ 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, string>
title={title}
result={result}
renderObjects={renderScrapedTags}
onChange={onChange}
newObjects={newObjects}
onCreateNew={onCreateNew}
/>
);
};

View File

@@ -0,0 +1,192 @@
import { useToast } from "src/hooks/Toast";
import * as GQL from "src/core/generated-graphql";
import {
useMovieCreate,
usePerformerCreate,
useStudioCreate,
useTagCreate,
} from "src/core/StashService";
import { ScrapeResult } from "./scrapeResult";
import { useIntl } from "react-intl";
import { scrapedPerformerToCreateInput } from "src/core/performers";
import { scrapedMovieToCreateInput } from "src/core/movies";
function useCreateObject<T>(
entityTypeID: string,
createFunc: (o: T) => Promise<void>
) {
const Toast = useToast();
const intl = useIntl();
async function createNewObject(o: T) {
try {
await createFunc(o);
Toast.success({
content: intl.formatMessage(
{ id: "toast.created_entity" },
{
entity: intl
.formatMessage({ id: entityTypeID })
.toLocaleLowerCase(),
}
),
});
} catch (e) {
Toast.error(e);
}
}
return createNewObject;
}
interface IUseCreateNewStudioProps {
scrapeResult: ScrapeResult<string>;
setScrapeResult: (scrapeResult: ScrapeResult<string>) => void;
setNewObject: (newObject: GQL.ScrapedStudio | undefined) => void;
}
export function useCreateScrapedStudio(props: IUseCreateNewStudioProps) {
const [createStudio] = useStudioCreate();
const { scrapeResult, setScrapeResult, setNewObject } = props;
async function createNewStudio(toCreate: GQL.ScrapedStudio) {
const result = await createStudio({
variables: {
input: {
name: toCreate.name,
url: toCreate.url,
},
},
});
// set the new studio as the value
setScrapeResult(scrapeResult.cloneWithValue(result.data!.studioCreate!.id));
setNewObject(undefined);
}
return useCreateObject("studio", createNewStudio);
}
interface IUseCreateNewPerformerProps {
scrapeResult: ScrapeResult<GQL.ScrapedPerformer[]>;
setScrapeResult: (scrapeResult: ScrapeResult<GQL.ScrapedPerformer[]>) => void;
newObjects: GQL.ScrapedPerformer[];
setNewObjects: (newObject: GQL.ScrapedPerformer[]) => void;
}
export function useCreateScrapedPerformer(props: IUseCreateNewPerformerProps) {
const [createPerformer] = usePerformerCreate();
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
async function createNewPerformer(toCreate: GQL.ScrapedPerformer) {
const input = scrapedPerformerToCreateInput(toCreate);
const result = await createPerformer({
variables: { input },
});
const newValue = [...(scrapeResult.newValue ?? [])];
if (result.data?.performerCreate)
newValue.push({
stored_id: result.data.performerCreate.id,
name: result.data.performerCreate.name,
});
// add the new performer to the new performers value
const performerClone = scrapeResult.cloneWithValue(newValue);
setScrapeResult(performerClone);
// remove the performer from the list
const newPerformersClone = newObjects.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);
setNewObjects(newPerformersClone);
}
return useCreateObject("performer", createNewPerformer);
}
interface IUseCreateNewObjectIDListProps<
T extends { name?: string | undefined | null }
> {
scrapeResult: ScrapeResult<string[]>;
setScrapeResult: (scrapeResult: ScrapeResult<string[]>) => void;
newObjects: T[];
setNewObjects: (newObject: T[]) => void;
}
function useCreateNewObjectIDList<
T extends { name?: string | undefined | null }
>(
entityTypeID: string,
props: IUseCreateNewObjectIDListProps<T>,
createObject: (toCreate: T) => Promise<string>
) {
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
async function createNewObject(toCreate: T) {
const newID = await createObject(toCreate);
// add the new object to the new objects value
const newResult = scrapeResult.cloneWithValue(scrapeResult.newValue);
if (!newResult.newValue) {
newResult.newValue = [];
}
newResult.newValue.push(newID);
setScrapeResult(newResult);
// remove the object from the list
const newObjectsClone = newObjects.concat();
const pIndex = newObjectsClone.findIndex((p) => p.name === toCreate.name);
if (pIndex === -1) throw new Error("Could not find object to remove");
newObjectsClone.splice(pIndex, 1);
setNewObjects(newObjectsClone);
}
return useCreateObject(entityTypeID, createNewObject);
}
export function useCreateScrapedMovie(
props: IUseCreateNewObjectIDListProps<GQL.ScrapedMovie>
) {
const [createMovie] = useMovieCreate();
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
const movieInput = scrapedMovieToCreateInput(toCreate);
const result = await createMovie({
variables: { input: movieInput },
});
return result.data?.movieCreate?.id ?? "";
}
return useCreateNewObjectIDList("movie", props, createNewMovie);
}
export function useCreateScrapedTag(
props: IUseCreateNewObjectIDListProps<GQL.ScrapedTag>
) {
const [createTag] = useTagCreate();
async function createNewTag(toCreate: GQL.ScrapedTag) {
const tagInput: GQL.TagCreateInput = { name: toCreate.name ?? "" };
const result = await createTag({
variables: {
input: tagInput,
},
});
return result.data?.tagCreate?.id ?? "";
}
return useCreateNewObjectIDList("tag", props, createNewTag);
}

View File

@@ -0,0 +1,71 @@
import isEqual from "lodash-es/isEqual";
import clone from "lodash-es/clone";
export class ScrapeResult<T> {
public newValue?: T;
public originalValue?: T;
public scraped: boolean = false;
public useNewValue: boolean = false;
public constructor(
originalValue?: T | null,
newValue?: T | null,
useNewValue?: boolean
) {
this.originalValue = originalValue ?? undefined;
this.newValue = newValue ?? undefined;
// NOTE: this means that zero values are treated as null
// this is incorrect for numbers and booleans, but correct for strings
const hasNewValue = !!this.newValue;
const valuesEqual = isEqual(originalValue, newValue);
this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual);
this.scraped = hasNewValue && !valuesEqual;
}
public setOriginalValue(value?: T) {
this.originalValue = value;
this.newValue = value;
}
public cloneWithValue(value?: T) {
const ret = clone(this);
ret.newValue = value;
ret.useNewValue = !isEqual(ret.newValue, ret.originalValue);
// #2691 - if we're setting the value, assume it should be treated as
// scraped
ret.scraped = true;
return ret;
}
public getNewValue() {
if (this.useNewValue) {
return this.newValue;
}
}
}
// for types where !!value is a valid value (boolean and number)
export class ZeroableScrapeResult<T> extends ScrapeResult<T> {
public constructor(
originalValue?: T | null,
newValue?: T | null,
useNewValue?: boolean
) {
super(originalValue, newValue, useNewValue);
const hasNewValue = this.newValue !== undefined;
const valuesEqual = isEqual(originalValue, newValue);
this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual);
this.scraped = hasNewValue && !valuesEqual;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function hasScrapedValues(values: ScrapeResult<any>[]) {
return values.some((r) => r.scraped);
}

View File

@@ -42,3 +42,27 @@ export function excludeFields(
} }
}); });
} }
export interface IHasStoredID {
stored_id?: string | null;
}
export function sortStoredIdObjects(
scrapedObjects?: IHasStoredID[]
): IHasStoredID[] | undefined {
if (!scrapedObjects) {
return undefined;
}
const ret = scrapedObjects.filter((p) => !!p.stored_id);
if (ret.length === 0) {
return undefined;
}
// sort by id numerically
ret.sort((a, b) => {
return parseInt(a.stored_id!, 10) - parseInt(b.stored_id!, 10);
});
return ret;
}