mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Movie select overhaul (#4563)
* Add ids to findMovies input * Use ids for other find interfaces * Update client side * Fix gallery select function * Replace movie select * Re-add creatable * Overhaul movie table * Remove and deprecated unused code
This commit is contained in:
@@ -25,16 +25,11 @@ import { StringListInput } from "../StringListInput";
|
||||
import { ImageSelector } from "../ImageSelector";
|
||||
import { ScrapeResult } from "./scrapeResult";
|
||||
|
||||
export interface IHasName {
|
||||
name: string | undefined;
|
||||
}
|
||||
|
||||
interface IScrapedFieldProps<T> {
|
||||
result: ScrapeResult<T>;
|
||||
}
|
||||
|
||||
interface IScrapedRowProps<T, V extends IHasName>
|
||||
extends IScrapedFieldProps<T> {
|
||||
interface IScrapedRowProps<T, V> extends IScrapedFieldProps<T> {
|
||||
className?: string;
|
||||
title: string;
|
||||
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
@@ -42,6 +37,7 @@ interface IScrapedRowProps<T, V extends IHasName>
|
||||
onChange: (value: ScrapeResult<T>) => void;
|
||||
newValues?: V[];
|
||||
onCreateNew?: (index: number) => void;
|
||||
getName?: (value: V) => string;
|
||||
}
|
||||
|
||||
function renderButtonIcon(selected: boolean) {
|
||||
@@ -55,9 +51,9 @@ function renderButtonIcon(selected: boolean) {
|
||||
);
|
||||
}
|
||||
|
||||
export const ScrapeDialogRow = <T, V extends IHasName>(
|
||||
props: IScrapedRowProps<T, V>
|
||||
) => {
|
||||
export const ScrapeDialogRow = <T, V>(props: IScrapedRowProps<T, V>) => {
|
||||
const { getName = () => "" } = props;
|
||||
|
||||
function handleSelectClick(isNew: boolean) {
|
||||
const ret = clone(props.result);
|
||||
ret.useNewValue = isNew;
|
||||
@@ -83,10 +79,10 @@ export const ScrapeDialogRow = <T, V extends IHasName>(
|
||||
<Badge
|
||||
className="tag-item"
|
||||
variant="secondary"
|
||||
key={t.name}
|
||||
key={getName(t)}
|
||||
onClick={() => props.onCreateNew!(i)}
|
||||
>
|
||||
{t.name}
|
||||
{getName(t)}
|
||||
<Button className="minimal ml-2">
|
||||
<Icon className="fa-fw" icon={faPlus} />
|
||||
</Button>
|
||||
@@ -173,6 +169,10 @@ const ScrapedInputGroup: React.FC<IScrapedInputGroupProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
function getNameString(value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
interface IScrapedInputGroupRowProps {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
@@ -206,6 +206,7 @@ export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -271,6 +272,7 @@ export const ScrapedStringListRow: React.FC<IScrapedStringListRowProps> = (
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -316,6 +318,7 @@ export const ScrapedTextAreaRow: React.FC<IScrapedInputGroupRowProps> = (
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -369,6 +372,7 @@ export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -412,6 +416,7 @@ export const ScrapedImagesRow: React.FC<IScrapedImagesRowProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -514,5 +519,6 @@ export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
|
||||
/>
|
||||
)}
|
||||
onChange={onChange}
|
||||
getName={getNameString}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React, { useMemo } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { MovieSelect } from "src/components/Shared/Select";
|
||||
import {
|
||||
ScrapeDialogRow,
|
||||
IHasName,
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { ScrapeDialogRow } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { PerformerSelect } from "src/components/Performers/PerformerSelect";
|
||||
import {
|
||||
ObjectScrapeResult,
|
||||
@@ -12,6 +8,7 @@ import {
|
||||
} from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||
import { TagSelect } from "src/components/Tags/TagSelect";
|
||||
import { StudioSelect } from "src/components/Studios/StudioSelect";
|
||||
import { MovieSelect } from "src/components/Movies/MovieSelect";
|
||||
|
||||
interface IScrapedStudioRow {
|
||||
title: string;
|
||||
@@ -21,6 +18,10 @@ interface IScrapedStudioRow {
|
||||
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
||||
}
|
||||
|
||||
function getObjectName<T extends { name: string }>(value: T) {
|
||||
return value.name;
|
||||
}
|
||||
|
||||
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
||||
title,
|
||||
result,
|
||||
@@ -76,28 +77,35 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
||||
onCreateNew={() => {
|
||||
if (onCreateNew && newStudio) onCreateNew(newStudio);
|
||||
}}
|
||||
getName={getObjectName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedObjectsRow<T, R> {
|
||||
interface IScrapedObjectsRow<T> {
|
||||
title: string;
|
||||
result: ScrapeResult<R[]>;
|
||||
onChange: (value: ScrapeResult<R[]>) => void;
|
||||
result: ScrapeResult<T[]>;
|
||||
onChange: (value: ScrapeResult<T[]>) => void;
|
||||
newObjects?: T[];
|
||||
onCreateNew?: (value: T) => void;
|
||||
renderObjects: (
|
||||
result: ScrapeResult<R[]>,
|
||||
result: ScrapeResult<T[]>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: R[]) => void
|
||||
onChange?: (value: T[]) => void
|
||||
) => JSX.Element;
|
||||
getName: (value: T) => string;
|
||||
}
|
||||
|
||||
export const ScrapedObjectsRow = <T extends IHasName, R>(
|
||||
props: IScrapedObjectsRow<T, R>
|
||||
) => {
|
||||
const { title, result, onChange, newObjects, onCreateNew, renderObjects } =
|
||||
props;
|
||||
export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => {
|
||||
const {
|
||||
title,
|
||||
result,
|
||||
onChange,
|
||||
newObjects,
|
||||
onCreateNew,
|
||||
renderObjects,
|
||||
getName,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
@@ -114,17 +122,18 @@ export const ScrapedObjectsRow = <T extends IHasName, R>(
|
||||
onCreateNew={(i) => {
|
||||
if (onCreateNew) onCreateNew(newObjects![i]);
|
||||
}}
|
||||
getName={getName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type IScrapedObjectRowImpl<T, R> = Omit<
|
||||
IScrapedObjectsRow<T, R>,
|
||||
"renderObjects"
|
||||
type IScrapedObjectRowImpl<T> = Omit<
|
||||
IScrapedObjectsRow<T>,
|
||||
"renderObjects" | "getName"
|
||||
>;
|
||||
|
||||
export const ScrapedPerformersRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedPerformer, GQL.ScrapedPerformer>
|
||||
IScrapedObjectRowImpl<GQL.ScrapedPerformer>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
const performersCopy = useMemo(() => {
|
||||
return (
|
||||
@@ -170,24 +179,21 @@ export const ScrapedPerformersRow: React.FC<
|
||||
);
|
||||
}
|
||||
|
||||
type PerformerType = GQL.ScrapedPerformer & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<PerformerType, GQL.ScrapedPerformer>
|
||||
<ScrapedObjectsRow<GQL.ScrapedPerformer>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedPerformers}
|
||||
onChange={onChange}
|
||||
newObjects={performersCopy}
|
||||
onCreateNew={onCreateNew}
|
||||
getName={(value) => value.name ?? ""}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedMoviesRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedMovie, string>
|
||||
IScrapedObjectRowImpl<GQL.ScrapedMovie>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
const moviesCopy = useMemo(() => {
|
||||
return (
|
||||
@@ -198,20 +204,25 @@ export const ScrapedMoviesRow: React.FC<
|
||||
);
|
||||
}, [newObjects]);
|
||||
|
||||
type MovieType = GQL.ScrapedMovie & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
function renderScrapedMovies(
|
||||
scrapeResult: ScrapeResult<string[]>,
|
||||
scrapeResult: ScrapeResult<GQL.ScrapedMovie[]>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string[]) => void
|
||||
onChangeFn?: (value: GQL.ScrapedMovie[]) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
const selectValue = value.map((p) => {
|
||||
const aliases: string[] = [];
|
||||
return {
|
||||
id: p.stored_id ?? "",
|
||||
name: p.name ?? "",
|
||||
aliases,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<MovieSelect
|
||||
isMulti
|
||||
@@ -219,28 +230,30 @@ export const ScrapedMoviesRow: React.FC<
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items.map((i) => i.id));
|
||||
// map the id back to stored_id
|
||||
onChangeFn(items.map((p) => ({ ...p, stored_id: p.id })));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
values={selectValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<MovieType, string>
|
||||
<ScrapedObjectsRow<GQL.ScrapedMovie>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedMovies}
|
||||
onChange={onChange}
|
||||
newObjects={moviesCopy}
|
||||
onCreateNew={onCreateNew}
|
||||
getName={(value) => value.name ?? ""}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedTagsRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedTag, GQL.ScrapedTag>
|
||||
IScrapedObjectRowImpl<GQL.ScrapedTag>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
function renderScrapedTags(
|
||||
scrapeResult: ScrapeResult<GQL.ScrapedTag[]>,
|
||||
@@ -278,13 +291,14 @@ export const ScrapedTagsRow: React.FC<
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<GQL.ScrapedTag, GQL.ScrapedTag>
|
||||
<ScrapedObjectsRow<GQL.ScrapedTag>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedTags}
|
||||
onChange={onChange}
|
||||
newObjects={newObjects}
|
||||
onCreateNew={onCreateNew}
|
||||
getName={getObjectName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -123,62 +123,41 @@ export function useCreateScrapedPerformer(
|
||||
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>
|
||||
export function useCreateScrapedMovie(
|
||||
props: IUseCreateNewObjectProps<GQL.ScrapedMovie>
|
||||
) {
|
||||
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
|
||||
const [createMovie] = useMovieCreate();
|
||||
|
||||
async function createNewObject(toCreate: T) {
|
||||
const newID = await createObject(toCreate);
|
||||
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
|
||||
const input = scrapedMovieToCreateInput(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);
|
||||
const result = await createMovie({
|
||||
variables: { input: input },
|
||||
});
|
||||
|
||||
const newValue = [...(scrapeResult.newValue ?? [])];
|
||||
if (result.data?.movieCreate)
|
||||
newValue.push({
|
||||
stored_id: result.data.movieCreate.id,
|
||||
name: result.data.movieCreate.name,
|
||||
});
|
||||
|
||||
// add the new object to the new object value
|
||||
const resultClone = scrapeResult.cloneWithValue(newValue);
|
||||
setScrapeResult(resultClone);
|
||||
|
||||
// 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");
|
||||
if (pIndex === -1) throw new Error("Could not find movie 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);
|
||||
return useCreateObject("movie", createNewMovie);
|
||||
}
|
||||
|
||||
export function useCreateScrapedTag(
|
||||
|
||||
Reference in New Issue
Block a user