Add stash ids to performer merge dialog (#6688)

* Move reused functions/components to separate files
* Add alwaysShow field to ScrapedDialogRow
* Add stash ids to performer merge dialog
* Reuse StashIDsField in TagMergeDialog
* Always show stash ids when available on scene and tag merge dialogs
This commit is contained in:
WithoutPants 2026-03-17 15:48:56 +11:00 committed by GitHub
parent f3c8e7ac9c
commit b2179cd723
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 79 additions and 40 deletions

View file

@ -20,13 +20,14 @@ import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
import { ScrapeDialog } from "../Shared/ScrapeDialog/ScrapeDialog";
import {
ScrapedCustomFieldRows,
ScrapeDialogRow,
ScrapedImageRow,
ScrapedInputGroupRow,
ScrapedStringListRow,
ScrapedTextAreaRow,
} from "../Shared/ScrapeDialog/ScrapeDialogRow";
import { ModalComponent } from "../Shared/Modal";
import { sortStoredIdObjects } from "src/utils/data";
import { sortStoredIdObjects, uniqIDStoredIDs } from "src/utils/data";
import {
CustomFieldScrapeResults,
ObjectListScrapeResult,
@ -40,6 +41,7 @@ import {
} from "./PerformerDetails/PerformerScrapeDialog";
import { PerformerSelect } from "./PerformerSelect";
import { uniq } from "lodash-es";
import { StashIDsField } from "../Shared/StashID";
type MergeOptions = {
values: GQL.PerformerUpdateInput;
@ -132,6 +134,8 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
)
);
const [stashIDs, setStashIDs] = useState(new ScrapeResult<GQL.StashId[]>([]));
const [image, setImage] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(dest.image_path)
);
@ -166,6 +170,10 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
setLoading(false);
}
// append dest to all so that if dest has stash_ids with the same
// endpoint, then it will be excluded first
const all = sources.concat(dest);
setName(
new ScrapeResult(dest.name, sources.find((s) => s.name)?.name, !dest.name)
);
@ -297,9 +305,8 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
);
setURLs(
new ScrapeResult(
dest.urls,
sources.find((s) => s.urls)?.urls,
!dest.urls?.length
dest.urls ?? [],
uniq(all.map((s) => s.urls ?? []).flat())
)
);
setGender(
@ -327,6 +334,25 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
!dest.details
)
);
setTags(
new ObjectListScrapeResult<GQL.ScrapedTag>(
sortStoredIdObjects(dest.tags.map(idToStoredID)),
uniqIDStoredIDs(all.map((s) => s.tags.map(idToStoredID)).flat())
)
);
setStashIDs(
new ScrapeResult(
dest.stash_ids,
all
.map((s) => s.stash_ids)
.flat()
.filter((s, index, a) => {
// remove entries with duplicate endpoints
return index === a.findIndex((ss) => ss.endpoint === s.endpoint);
})
)
);
setImage(
new ScrapeResult(
dest.image_path,
@ -583,6 +609,19 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
result={details}
onChange={(value) => setDetails(value)}
/>
<ScrapeDialogRow
field="stash_ids"
title={intl.formatMessage({ id: "stash_id" })}
result={stashIDs}
originalField={
<StashIDsField values={stashIDs?.originalValue ?? []} />
}
newField={<StashIDsField values={stashIDs?.newValue ?? []} />}
onChange={(value) => setStashIDs(value)}
alwaysShow={
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
}
/>
<ScrapedImageRow
field="image"
title={intl.formatMessage({ id: "performer_image" })}
@ -639,6 +678,7 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
circumcised: stringToCircumcised(circumcised.getNewValue()),
tag_ids: tags.getNewValue()?.map((t) => t.stored_id!),
details: details.getNewValue(),
stash_ids: stashIDs.getNewValue(),
image: coverImage,
custom_fields: {
partial: Object.fromEntries(

View file

@ -26,7 +26,7 @@ import { ScrapeDialog } from "../Shared/ScrapeDialog/ScrapeDialog";
import { clone, uniq } from "lodash-es";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { ModalComponent } from "../Shared/Modal";
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
import { sortStoredIdObjects, uniqIDStoredIDs } from "src/utils/data";
import {
CustomFieldScrapeResults,
ObjectListScrapeResult,
@ -41,25 +41,7 @@ import {
ScrapedTagsRow,
} from "../Shared/ScrapeDialog/ScrapedObjectsRow";
import { Scene, SceneSelect } from "src/components/Scenes/SceneSelect";
import { StashIDPill } from "src/components/Shared/StashID";
interface IStashIDsField {
values: GQL.StashId[];
}
const StashIDsField: React.FC<IStashIDsField> = ({ values }) => {
if (!values.length) return null;
return (
<ul className="pl-0 mw-100">
{values.map((v) => (
<li key={v.stash_id} className="row no-gutters">
<StashIDPill linkType="scenes" stashID={v} />
</li>
))}
</ul>
);
};
import { StashIDsField } from "../Shared/StashID";
type MergeOptions = {
values: GQL.SceneUpdateInput;
@ -143,12 +125,6 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
return ret;
}
function uniqIDStoredIDs<T extends IHasStoredID>(objs: T[]) {
return objs.filter((o, i) => {
return objs.findIndex((oo) => oo.stored_id === o.stored_id) === i;
});
}
const [performers, setPerformers] = useState<
ObjectListScrapeResult<GQL.ScrapedPerformer>
>(
@ -615,6 +591,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
}
newField={<StashIDsField values={stashIDs?.newValue ?? []} />}
onChange={(value) => setStashIDs(value)}
alwaysShow={
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
}
/>
<ScrapedImageRow
field="cover_image"

View file

@ -40,6 +40,7 @@ interface IScrapedRowProps<T> extends IScrapedFieldProps<T> {
newField: React.ReactNode;
onChange: (value: ScrapeResult<T>) => void;
newValues?: React.ReactNode;
alwaysShow?: boolean;
}
export const ScrapeDialogRow = <T,>(props: IScrapedRowProps<T>) => {
@ -51,7 +52,7 @@ export const ScrapeDialogRow = <T,>(props: IScrapedRowProps<T>) => {
props.onChange(ret);
}
if (!props.result.scraped && !props.newValues) {
if (!props.result.scraped && !props.newValues && !props.alwaysShow) {
return <></>;
}

View file

@ -31,3 +31,21 @@ export const StashIDPill: React.FC<{
</span>
);
};
interface IStashIDsField {
values: StashId[];
}
export const StashIDsField: React.FC<IStashIDsField> = ({ values }) => {
if (!values.length) return null;
return (
<ul className="pl-0 mw-100">
{values.map((v) => (
<li key={v.stash_id} className="row no-gutters">
<StashIDPill linkType="scenes" stashID={v} />
</li>
))}
</ul>
);
};

View file

@ -28,16 +28,8 @@ import {
ScrapedTextAreaRow,
} from "../Shared/ScrapeDialog/ScrapeDialogRow";
import { ScrapedTagsRow } from "../Shared/ScrapeDialog/ScrapedObjectsRow";
import { StringListSelect } from "../Shared/Select";
import { ScrapeDialog } from "../Shared/ScrapeDialog/ScrapeDialog";
interface IStashIDsField {
values: GQL.StashId[];
}
const StashIDsField: React.FC<IStashIDsField> = ({ values }) => {
return <StringListSelect value={values.map((v) => v.stash_id)} />;
};
import { StashIDsField } from "../Shared/StashID";
interface ITagMergeDetailsProps {
sources: GQL.TagDataFragment[];
@ -333,6 +325,9 @@ const TagMergeDetails: React.FC<ITagMergeDetailsProps> = ({
}
newField={<StashIDsField values={stashIDs?.newValue ?? []} />}
onChange={(value) => setStashIDs(value)}
alwaysShow={
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
}
/>
<ScrapedImageRow
field="image"

View file

@ -70,3 +70,9 @@ export function sortStoredIdObjects<T extends IHasStoredID>(
return ret;
}
export function uniqIDStoredIDs<T extends IHasStoredID>(objs: T[]) {
return objs.filter((o, i) => {
return objs.findIndex((oo) => oo.stored_id === o.stored_id) === i;
});
}