From b2179cd7230c21460bf2a057badeefe1ac4174e8 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:48:56 +1100 Subject: [PATCH] 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 --- .../Performers/PerformerMergeDialog.tsx | 48 +++++++++++++++++-- .../components/Scenes/SceneMergeDialog.tsx | 31 ++---------- .../Shared/ScrapeDialog/ScrapeDialogRow.tsx | 3 +- ui/v2.5/src/components/Shared/StashID.tsx | 18 +++++++ .../src/components/Tags/TagMergeDialog.tsx | 13 ++--- ui/v2.5/src/utils/data.ts | 6 +++ 6 files changed, 79 insertions(+), 40 deletions(-) diff --git a/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx index e4699c8e0..0d42dd6ed 100644 --- a/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx +++ b/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx @@ -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 = ({ ) ); + const [stashIDs, setStashIDs] = useState(new ScrapeResult([])); + const [image, setImage] = useState>( new ScrapeResult(dest.image_path) ); @@ -166,6 +170,10 @@ const PerformerMergeDetails: React.FC = ({ 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 = ({ ); 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 = ({ !dest.details ) ); + setTags( + new ObjectListScrapeResult( + 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 = ({ result={details} onChange={(value) => setDetails(value)} /> + + } + newField={} + onChange={(value) => setStashIDs(value)} + alwaysShow={ + !!stashIDs.originalValue?.length || !!stashIDs.newValue?.length + } + /> = ({ 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( diff --git a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx index c38b27f07..29ae93143 100644 --- a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx @@ -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 = ({ values }) => { - if (!values.length) return null; - - return ( -
    - {values.map((v) => ( -
  • - -
  • - ))} -
- ); -}; +import { StashIDsField } from "../Shared/StashID"; type MergeOptions = { values: GQL.SceneUpdateInput; @@ -143,12 +125,6 @@ const SceneMergeDetails: React.FC = ({ return ret; } - function uniqIDStoredIDs(objs: T[]) { - return objs.filter((o, i) => { - return objs.findIndex((oo) => oo.stored_id === o.stored_id) === i; - }); - } - const [performers, setPerformers] = useState< ObjectListScrapeResult >( @@ -615,6 +591,9 @@ const SceneMergeDetails: React.FC = ({ } newField={} onChange={(value) => setStashIDs(value)} + alwaysShow={ + !!stashIDs.originalValue?.length || !!stashIDs.newValue?.length + } /> extends IScrapedFieldProps { newField: React.ReactNode; onChange: (value: ScrapeResult) => void; newValues?: React.ReactNode; + alwaysShow?: boolean; } export const ScrapeDialogRow = (props: IScrapedRowProps) => { @@ -51,7 +52,7 @@ export const ScrapeDialogRow = (props: IScrapedRowProps) => { props.onChange(ret); } - if (!props.result.scraped && !props.newValues) { + if (!props.result.scraped && !props.newValues && !props.alwaysShow) { return <>; } diff --git a/ui/v2.5/src/components/Shared/StashID.tsx b/ui/v2.5/src/components/Shared/StashID.tsx index 847dd7ab2..be9ee0fba 100644 --- a/ui/v2.5/src/components/Shared/StashID.tsx +++ b/ui/v2.5/src/components/Shared/StashID.tsx @@ -31,3 +31,21 @@ export const StashIDPill: React.FC<{ ); }; + +interface IStashIDsField { + values: StashId[]; +} + +export const StashIDsField: React.FC = ({ values }) => { + if (!values.length) return null; + + return ( +
    + {values.map((v) => ( +
  • + +
  • + ))} +
+ ); +}; diff --git a/ui/v2.5/src/components/Tags/TagMergeDialog.tsx b/ui/v2.5/src/components/Tags/TagMergeDialog.tsx index a66ce5789..b155f46e2 100644 --- a/ui/v2.5/src/components/Tags/TagMergeDialog.tsx +++ b/ui/v2.5/src/components/Tags/TagMergeDialog.tsx @@ -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 = ({ values }) => { - return v.stash_id)} />; -}; +import { StashIDsField } from "../Shared/StashID"; interface ITagMergeDetailsProps { sources: GQL.TagDataFragment[]; @@ -333,6 +325,9 @@ const TagMergeDetails: React.FC = ({ } newField={} onChange={(value) => setStashIDs(value)} + alwaysShow={ + !!stashIDs.originalValue?.length || !!stashIDs.newValue?.length + } /> ( return ret; } + +export function uniqIDStoredIDs(objs: T[]) { + return objs.filter((o, i) => { + return objs.findIndex((oo) => oo.stored_id === o.stored_id) === i; + }); +}