diff --git a/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx index 0c3c6d8fbb..16e330da27 100644 --- a/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx +++ b/ui/v2.5/src/components/Performers/PerformerMergeDialog.tsx @@ -46,6 +46,7 @@ import { import { PerformerSelect } from "./PerformerSelect"; import { uniq } from "lodash-es"; import { StashIDsField } from "../Shared/StashID"; +import { PatchComponent } from "src/patch"; type MergeOptions = { values: GQL.PerformerUpdateInput; @@ -728,190 +729,186 @@ interface IPerformerMergeModalProps { performers: GQL.SelectPerformerDataFragment[]; } -export const PerformerMergeModal: React.FC = ({ - show, - onClose, - performers, -}) => { - const [sourcePerformers, setSourcePerformers] = useState< - GQL.SelectPerformerDataFragment[] - >([]); - const [destPerformer, setDestPerformer] = useState< - GQL.SelectPerformerDataFragment[] - >([]); - - const [loadedSources, setLoadedSources] = useState< - GQL.PerformerDataFragment[] - >([]); - const [loadedDest, setLoadedDest] = useState(); +export const PerformerMergeModal: React.FC = + PatchComponent("PerformerMergeModal", ({ show, onClose, performers }) => { + const [sourcePerformers, setSourcePerformers] = useState< + GQL.SelectPerformerDataFragment[] + >([]); + const [destPerformer, setDestPerformer] = useState< + GQL.SelectPerformerDataFragment[] + >([]); + + const [loadedSources, setLoadedSources] = useState< + GQL.PerformerDataFragment[] + >([]); + const [loadedDest, setLoadedDest] = useState(); + + const [running, setRunning] = useState(false); + const [secondStep, setSecondStep] = useState(false); + + const intl = useIntl(); + const Toast = useToast(); + + const title = intl.formatMessage({ + id: "actions.merge", + }); + + const srcIDs = useMemo( + () => sourcePerformers.map((s) => s.id), + [sourcePerformers] + ); + const destID = useMemo( + () => (destPerformer[0] ? [destPerformer[0].id] : []), + [destPerformer] + ); - const [running, setRunning] = useState(false); - const [secondStep, setSecondStep] = useState(false); + useEffect(() => { + if (performers.length > 0) { + // set the first performer as the destination, others as source + setDestPerformer([performers[0]]); - const intl = useIntl(); - const Toast = useToast(); + if (performers.length > 1) { + setSourcePerformers(performers.slice(1)); + } + } + }, [performers]); - const title = intl.formatMessage({ - id: "actions.merge", - }); + async function loadPerformers() { + const performerIDs = sourcePerformers.map((s) => parseInt(s.id, 10)); + performerIDs.push(parseInt(destPerformer[0].id, 10)); + const query = await queryFindPerformersByID(performerIDs); + const { performers: loadedPerformers } = query.data.findPerformers; - const srcIDs = useMemo( - () => sourcePerformers.map((s) => s.id), - [sourcePerformers] - ); - const destID = useMemo( - () => (destPerformer[0] ? [destPerformer[0].id] : []), - [destPerformer] - ); - - useEffect(() => { - if (performers.length > 0) { - // set the first performer as the destination, others as source - setDestPerformer([performers[0]]); + setLoadedDest(loadedPerformers.find((s) => s.id === destPerformer[0].id)); + setLoadedSources( + loadedPerformers.filter((s) => s.id !== destPerformer[0].id) + ); + setSecondStep(true); + } - if (performers.length > 1) { - setSourcePerformers(performers.slice(1)); + async function onMerge(options: MergeOptions) { + const { values } = options; + try { + setRunning(true); + const result = await mutatePerformerMerge( + destPerformer[0].id, + sourcePerformers.map((s) => s.id), + values + ); + if (result.data?.performerMerge) { + Toast.success(intl.formatMessage({ id: "toast.merged_performers" })); + onClose(destPerformer[0].id); + } + } catch (e) { + Toast.error(e); + } finally { + setRunning(false); } } - }, [performers]); - - async function loadPerformers() { - const performerIDs = sourcePerformers.map((s) => parseInt(s.id, 10)); - performerIDs.push(parseInt(destPerformer[0].id, 10)); - const query = await queryFindPerformersByID(performerIDs); - const { performers: loadedPerformers } = query.data.findPerformers; - setLoadedDest(loadedPerformers.find((s) => s.id === destPerformer[0].id)); - setLoadedSources( - loadedPerformers.filter((s) => s.id !== destPerformer[0].id) - ); - setSecondStep(true); - } + function canMerge() { + return sourcePerformers.length > 0 && destPerformer.length !== 0; + } - async function onMerge(options: MergeOptions) { - const { values } = options; - try { - setRunning(true); - const result = await mutatePerformerMerge( - destPerformer[0].id, - sourcePerformers.map((s) => s.id), - values - ); - if (result.data?.performerMerge) { - Toast.success(intl.formatMessage({ id: "toast.merged_performers" })); - onClose(destPerformer[0].id); + function switchPerformers() { + if (sourcePerformers.length && destPerformer.length) { + const newDest = sourcePerformers[0]; + setSourcePerformers([...sourcePerformers.slice(1), destPerformer[0]]); + setDestPerformer([newDest]); } - onClose(); - } catch (e) { - Toast.error(e); - } finally { - setRunning(false); } - } - - function canMerge() { - return sourcePerformers.length > 0 && destPerformer.length !== 0; - } - function switchPerformers() { - if (sourcePerformers.length && destPerformer.length) { - const newDest = sourcePerformers[0]; - setSourcePerformers([...sourcePerformers.slice(1), destPerformer[0]]); - setDestPerformer([newDest]); + if (secondStep && destPerformer.length > 0) { + return ( + { + setSecondStep(false); + if (values) { + onMerge(values); + } else { + onClose(); + } + }} + /> + ); } - } - if (secondStep && destPerformer.length > 0) { return ( - { - setSecondStep(false); - if (values) { - onMerge(values); - } else { - onClose(); - } + loadPerformers(), }} - /> - ); - } - - return ( - loadPerformers(), - }} - disabled={!canMerge()} - cancel={{ - variant: "secondary", - onClick: () => onClose(), - }} - isRunning={running} - > -
-
- - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "dialogs.merge.source" }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setSourcePerformers(items)} - values={sourcePerformers} - menuPortalTarget={document.body} - excludeIds={destID} - /> - - - - - - - {FormUtils.renderLabel({ - title: intl.formatMessage({ - id: "dialogs.merge.destination", - }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setDestPerformer(items)} - values={destPerformer} - menuPortalTarget={document.body} - excludeIds={srcIDs} - /> - - + + + + {FormUtils.renderLabel({ + title: intl.formatMessage({ + id: "dialogs.merge.destination", + }), + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + setDestPerformer(items)} + values={destPerformer} + menuPortalTarget={document.body} + excludeIds={srcIDs} + /> + + +
- -
- ); -}; +
+ ); + }); diff --git a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx index c1dfe512d1..ef781cddb6 100644 --- a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx @@ -46,6 +46,7 @@ import { } from "../Shared/ScrapeDialog/ScrapedObjectsRow"; import { Scene, SceneSelect } from "src/components/Scenes/SceneSelect"; import { StashIDsField } from "../Shared/StashID"; +import { PatchComponent } from "src/patch"; type MergeOptions = { values: GQL.SceneUpdateInput; @@ -699,182 +700,180 @@ interface ISceneMergeModalProps { scenes: { id: string; title: string }[]; } -export const SceneMergeModal: React.FC = ({ - show, - onClose, - scenes, -}) => { - const [sourceScenes, setSourceScenes] = useState([]); - const [destScene, setDestScene] = useState([]); +export const SceneMergeModal: React.FC = PatchComponent( + "SceneMergeModal", + ({ show, onClose, scenes }) => { + const [sourceScenes, setSourceScenes] = useState([]); + const [destScene, setDestScene] = useState([]); - const [loadedSources, setLoadedSources] = useState( - [] - ); - const [loadedDest, setLoadedDest] = useState(); + const [loadedSources, setLoadedSources] = useState( + [] + ); + const [loadedDest, setLoadedDest] = useState(); - const [running, setRunning] = useState(false); - const [secondStep, setSecondStep] = useState(false); + const [running, setRunning] = useState(false); + const [secondStep, setSecondStep] = useState(false); - const intl = useIntl(); - const Toast = useToast(); + const intl = useIntl(); + const Toast = useToast(); - const title = intl.formatMessage({ - id: "actions.merge", - }); + const title = intl.formatMessage({ + id: "actions.merge", + }); - const srcIDs = useMemo(() => sourceScenes.map((s) => s.id), [sourceScenes]); - const destID = useMemo( - () => (destScene[0] ? [destScene[0].id] : []), - [destScene] - ); + const srcIDs = useMemo(() => sourceScenes.map((s) => s.id), [sourceScenes]); + const destID = useMemo( + () => (destScene[0] ? [destScene[0].id] : []), + [destScene] + ); - useEffect(() => { - if (scenes.length > 0) { - // set the first scene as the destination, others as source - setDestScene([scenes[0]]); + useEffect(() => { + if (scenes.length > 0) { + // set the first scene as the destination, others as source + setDestScene([scenes[0]]); - if (scenes.length > 1) { - setSourceScenes(scenes.slice(1)); + if (scenes.length > 1) { + setSourceScenes(scenes.slice(1)); + } } - } - }, [scenes]); + }, [scenes]); - async function loadScenes() { - const sceneIDs = sourceScenes.map((s) => parseInt(s.id, 10)); - sceneIDs.push(parseInt(destScene[0].id, 10)); - const query = await queryFindFullScenesByID(sceneIDs); - const { scenes: loadedScenes } = query.data.findScenes; + async function loadScenes() { + const sceneIDs = sourceScenes.map((s) => parseInt(s.id, 10)); + sceneIDs.push(parseInt(destScene[0].id, 10)); + const query = await queryFindFullScenesByID(sceneIDs); + const { scenes: loadedScenes } = query.data.findScenes; - setLoadedDest(loadedScenes.find((s) => s.id === destScene[0].id)); - setLoadedSources(loadedScenes.filter((s) => s.id !== destScene[0].id)); - setSecondStep(true); - } + setLoadedDest(loadedScenes.find((s) => s.id === destScene[0].id)); + setLoadedSources(loadedScenes.filter((s) => s.id !== destScene[0].id)); + setSecondStep(true); + } - async function onMerge(options: MergeOptions) { - const { values, includeViewHistory, includeOHistory } = options; - try { - setRunning(true); - const result = await mutateSceneMerge( - destScene[0].id, - sourceScenes.map((s) => s.id), - values, - includeViewHistory, - includeOHistory - ); - if (result.data?.sceneMerge) { - Toast.success(intl.formatMessage({ id: "toast.merged_scenes" })); - onClose(destScene[0].id); + async function onMerge(options: MergeOptions) { + const { values, includeViewHistory, includeOHistory } = options; + try { + setRunning(true); + const result = await mutateSceneMerge( + destScene[0].id, + sourceScenes.map((s) => s.id), + values, + includeViewHistory, + includeOHistory + ); + if (result.data?.sceneMerge) { + Toast.success(intl.formatMessage({ id: "toast.merged_scenes" })); + onClose(destScene[0].id); + } + } catch (e) { + Toast.error(e); + } finally { + setRunning(false); } - onClose(); - } catch (e) { - Toast.error(e); - } finally { - setRunning(false); } - } - function canMerge() { - return sourceScenes.length > 0 && destScene.length !== 0; - } + function canMerge() { + return sourceScenes.length > 0 && destScene.length !== 0; + } - function switchScenes() { - if (sourceScenes.length && destScene.length) { - const newDest = sourceScenes[0]; - setSourceScenes([...sourceScenes.slice(1), destScene[0]]); - setDestScene([newDest]); + function switchScenes() { + if (sourceScenes.length && destScene.length) { + const newDest = sourceScenes[0]; + setSourceScenes([...sourceScenes.slice(1), destScene[0]]); + setDestScene([newDest]); + } + } + + if (secondStep && destScene.length > 0) { + return ( + { + setSecondStep(false); + if (values) { + onMerge(values); + } else { + onClose(); + } + }} + /> + ); } - } - if (secondStep && destScene.length > 0) { return ( - { - setSecondStep(false); - if (values) { - onMerge(values); - } else { - onClose(); - } + loadScenes(), }} - /> - ); - } - - return ( - loadScenes(), - }} - disabled={!canMerge()} - cancel={{ - variant: "secondary", - onClick: () => onClose(), - }} - isRunning={running} - > -
-
- - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "dialogs.merge.source" }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setSourceScenes(items)} - values={sourceScenes} - menuPortalTarget={document.body} - excludeIds={destID} - /> - - - - - - - {FormUtils.renderLabel({ - title: intl.formatMessage({ - id: "dialogs.merge.destination", - }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setDestScene(items)} - values={destScene} - menuPortalTarget={document.body} - excludeIds={srcIDs} - /> - - + + + + {FormUtils.renderLabel({ + title: intl.formatMessage({ + id: "dialogs.merge.destination", + }), + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + setDestScene(items)} + values={destScene} + menuPortalTarget={document.body} + excludeIds={srcIDs} + /> + + +
- -
- ); -}; +
+ ); + } +); diff --git a/ui/v2.5/src/components/Tags/TagMergeDialog.tsx b/ui/v2.5/src/components/Tags/TagMergeDialog.tsx index 41e6a04efd..6e3e3786f0 100644 --- a/ui/v2.5/src/components/Tags/TagMergeDialog.tsx +++ b/ui/v2.5/src/components/Tags/TagMergeDialog.tsx @@ -30,6 +30,7 @@ import { import { ScrapedTagsRow } from "../Shared/ScrapeDialog/ScrapedObjectsRow"; import { ScrapeDialog } from "../Shared/ScrapeDialog/ScrapeDialog"; import { StashIDsField } from "../Shared/StashID"; +import { PatchComponent } from "src/patch"; interface ITagMergeDetailsProps { sources: GQL.TagDataFragment[]; @@ -406,187 +407,188 @@ interface ITagMergeModalProps { tags: Tag[]; } -export const TagMergeModal: React.FC = ({ - show, - onClose, - tags, -}) => { - const [src, setSrc] = useState([]); - const [dest, setDest] = useState(null); +export const TagMergeModal: React.FC = PatchComponent( + "TagMergeModal", + ({ show, onClose, tags }) => { + const [src, setSrc] = useState([]); + const [dest, setDest] = useState(null); - const [loadedSources, setLoadedSources] = useState([]); - const [loadedDest, setLoadedDest] = useState(); + const [loadedSources, setLoadedSources] = useState( + [] + ); + const [loadedDest, setLoadedDest] = useState(); - const [secondStep, setSecondStep] = useState(false); + const [secondStep, setSecondStep] = useState(false); - const [running, setRunning] = useState(false); + const [running, setRunning] = useState(false); - const [mergeTags] = useTagsMerge(); + const [mergeTags] = useTagsMerge(); - const intl = useIntl(); - const Toast = useToast(); + const intl = useIntl(); + const Toast = useToast(); - const title = intl.formatMessage({ - id: "actions.merge", - }); + const title = intl.formatMessage({ + id: "actions.merge", + }); - const srcIDs = useMemo(() => src.map((s) => s.id), [src]); - const destID = useMemo(() => (dest ? [dest.id] : []), [dest]); + const srcIDs = useMemo(() => src.map((s) => s.id), [src]); + const destID = useMemo(() => (dest ? [dest.id] : []), [dest]); - useEffect(() => { - if (tags.length > 0) { - setDest(tags[0]); - setSrc(tags.slice(1)); - } - }, [tags]); - - async function loadTags() { - try { - const tagIDs = src.map((s) => s.id); - tagIDs.push(dest!.id); - const query = await queryFindTagsByID(tagIDs); - const { tags: loadedTags } = query.data.findTags; - - setLoadedDest(loadedTags.find((s) => s.id === dest!.id)); - setLoadedSources(loadedTags.filter((s) => s.id !== dest!.id)); - setSecondStep(true); - } catch (e) { - Toast.error(e); - return; + useEffect(() => { + if (tags.length > 0) { + setDest(tags[0]); + setSrc(tags.slice(1)); + } + }, [tags]); + + async function loadTags() { + try { + const tagIDs = src.map((s) => s.id); + tagIDs.push(dest!.id); + const query = await queryFindTagsByID(tagIDs); + const { tags: loadedTags } = query.data.findTags; + + setLoadedDest(loadedTags.find((s) => s.id === dest!.id)); + setLoadedSources(loadedTags.filter((s) => s.id !== dest!.id)); + setSecondStep(true); + } catch (e) { + Toast.error(e); + return; + } } - } - async function onMerge(values: GQL.TagUpdateInput) { - if (!dest) return; - - const source = src.map((s) => s.id); - const destination = dest.id; - - try { - setRunning(true); - const result = await mergeTags({ - variables: { - source, - destination, - values, - }, - }); - if (result.data?.tagsMerge) { - Toast.success(intl.formatMessage({ id: "toast.merged_tags" })); - onClose(dest.id); + async function onMerge(values: GQL.TagUpdateInput) { + if (!dest) return; + + const source = src.map((s) => s.id); + const destination = dest.id; + + try { + setRunning(true); + const result = await mergeTags({ + variables: { + source, + destination, + values, + }, + }); + if (result.data?.tagsMerge) { + Toast.success(intl.formatMessage({ id: "toast.merged_tags" })); + onClose(dest.id); + } + } catch (e) { + Toast.error(e); + } finally { + setRunning(false); } - } catch (e) { - Toast.error(e); - } finally { - setRunning(false); } - } - function canMerge() { - return src.length > 0 && dest !== null; - } + function canMerge() { + return src.length > 0 && dest !== null; + } - function switchTags() { - if (src.length && dest !== null) { - const newDest = src[0]; - setSrc([...src.slice(1), dest]); - setDest(newDest); + function switchTags() { + if (src.length && dest !== null) { + const newDest = src[0]; + setSrc([...src.slice(1), dest]); + setDest(newDest); + } + } + + if (secondStep && dest) { + return ( + { + setSecondStep(false); + if (values) { + onMerge(values); + } else { + onClose(); + } + }} + /> + ); } - } - if (secondStep && dest) { return ( - { - setSecondStep(false); - if (values) { - onMerge(values); - } else { - onClose(); - } + loadTags(), }} - /> - ); - } - - return ( - loadTags(), - }} - disabled={!canMerge()} - cancel={{ - variant: "secondary", - onClick: () => onClose(), - }} - isRunning={running} - > -
-
- - {FormUtils.renderLabel({ - title: intl.formatMessage({ id: "dialogs.merge.source" }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setSrc(items)} - values={src} - excludeIds={destID} - menuPortalTarget={document.body} - /> - - - - - - - {FormUtils.renderLabel({ - title: intl.formatMessage({ - id: "dialogs.merge.destination", - }), - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setDest(items[0])} - values={dest ? [dest] : undefined} - excludeIds={srcIDs} - menuPortalTarget={document.body} - /> - - + + + + {FormUtils.renderLabel({ + title: intl.formatMessage({ + id: "dialogs.merge.destination", + }), + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + setDest(items[0])} + values={dest ? [dest] : undefined} + excludeIds={srcIDs} + menuPortalTarget={document.body} + /> + + +
- -
- ); -}; +
+ ); + } +); diff --git a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md index 5a5e301a60..a711fde63f 100644 --- a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md +++ b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md @@ -299,6 +299,7 @@ Returns `void`. - `PerformerIDSelect` - `PerformerImagesPanel` - `PerformerList` +- `PerformerMergeModal` - `PerformerPage` - `PerformerRecommendationRow` - `PerformerScenesPanel` @@ -327,6 +328,7 @@ Returns `void`. - `SceneMarkerCardsGrid` - `SceneMarkerList` - `SceneMarkerRecommendationRow` +- `SceneMergeModal` - `SceneList` - `ScenePage` - `ScenePage.TabContent` @@ -362,6 +364,7 @@ Returns `void`. - `TagIDSelect` - `TagLink` - `TagList` +- `TagMergeModal` - `TagPage` - `TagRecommendationRow` - `TagSelect` diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index ccc0a4c614..9ed0cabb95 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -727,6 +727,7 @@ declare namespace PluginApi { PerformerIDSelect: React.FC; PerformerImagesPanel: React.FC; PerformerList: React.FC; + PerformerMergeModal: React.FC; PerformerPage: React.FC; PerformerRecommendationRow: React.FC; PerformerScenesPanel: React.FC; @@ -758,6 +759,7 @@ declare namespace PluginApi { SceneMarkerCardGrid: React.FC; SceneMarkerList: React.FC; SceneMarkerRecommendationRow: React.FC; + SceneMergeModal: React.FC; SceneRecommendationRow: React.FC; SelectSetting: React.FC; Setting: React.FC; @@ -784,6 +786,7 @@ declare namespace PluginApi { TagCardGrid: React.FC; TagLink: React.FC; TagList: React.FC; + TagMergeModal: React.FC; TagPage: React.FC; TagRecommendationRow: React.FC; TagSelect: React.FC; diff --git a/ui/v2.5/src/pluginApi.tsx b/ui/v2.5/src/pluginApi.tsx index bdb4d5ce2c..44626f9e5e 100644 --- a/ui/v2.5/src/pluginApi.tsx +++ b/ui/v2.5/src/pluginApi.tsx @@ -112,6 +112,10 @@ export const PluginApi = { GalleryViewer: () => import("src/components/Galleries/GalleryViewer"), DeleteScenesDialog: () => import("./components/Scenes/DeleteScenesDialog"), + SceneMergeDialog: () => import("./components/Scenes/SceneMergeDialog"), + PerformerMergeDialog: () => + import("./components/Performers/PerformerMergeDialog"), + TagMergeDialog: () => import("./components/Tags/TagMergeDialog"), SceneList: () => import("./components/Scenes/SceneList"), SceneMarkerList: () => import("./components/Scenes/SceneMarkerList"), Scene: () => import("./components/Scenes/SceneDetails/Scene"),