mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
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:
@@ -8,10 +8,9 @@ import {
|
||||
FormControl,
|
||||
Badge,
|
||||
} from "react-bootstrap";
|
||||
import { CollapseButton } from "./CollapseButton";
|
||||
import { Icon } from "./Icon";
|
||||
import { ModalComponent } from "./Modal";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { CollapseButton } from "../CollapseButton";
|
||||
import { Icon } from "../Icon";
|
||||
import { ModalComponent } from "../Modal";
|
||||
import clone from "lodash-es/clone";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
@@ -21,78 +20,10 @@ import {
|
||||
faTimes,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { getCountryByISO } from "src/utils/country";
|
||||
import { CountrySelect } from "./CountrySelect";
|
||||
import { StringListInput } from "./StringListInput";
|
||||
import { ImageSelector } from "./ImageSelector";
|
||||
|
||||
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);
|
||||
}
|
||||
import { CountrySelect } from "../CountrySelect";
|
||||
import { StringListInput } from "../StringListInput";
|
||||
import { ImageSelector } from "../ImageSelector";
|
||||
import { ScrapeResult } from "./scrapeResult";
|
||||
|
||||
export interface IHasName {
|
||||
name: string | undefined;
|
||||
269
ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx
Normal file
269
ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
192
ui/v2.5/src/components/Shared/ScrapeDialog/createObjects.ts
Normal file
192
ui/v2.5/src/components/Shared/ScrapeDialog/createObjects.ts
Normal 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);
|
||||
}
|
||||
71
ui/v2.5/src/components/Shared/ScrapeDialog/scrapeResult.ts
Normal file
71
ui/v2.5/src/components/Shared/ScrapeDialog/scrapeResult.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user