mirror of
https://github.com/stashapp/stash.git
synced 2026-04-24 07:53:31 +02:00
Add tag merge value dialog to choose values when merging
This commit is contained in:
parent
d1f16907cf
commit
ff6996680c
8 changed files with 501 additions and 48 deletions
|
|
@ -34,6 +34,8 @@ fragment TagData on Tag {
|
|||
children {
|
||||
...SlimTagData
|
||||
}
|
||||
|
||||
custom_fields
|
||||
}
|
||||
|
||||
fragment SelectTagData on Tag {
|
||||
|
|
|
|||
|
|
@ -24,8 +24,14 @@ mutation BulkTagUpdate($input: BulkTagUpdateInput!) {
|
|||
}
|
||||
}
|
||||
|
||||
mutation TagsMerge($source: [ID!]!, $destination: ID!) {
|
||||
tagsMerge(input: { source: $source, destination: $destination }) {
|
||||
mutation TagsMerge(
|
||||
$source: [ID!]!
|
||||
$destination: ID!
|
||||
$values: TagUpdateInput
|
||||
) {
|
||||
tagsMerge(
|
||||
input: { source: $source, destination: $destination, values: $values }
|
||||
) {
|
||||
...TagData
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
query FindTags($filter: FindFilterType, $tag_filter: TagFilterType) {
|
||||
findTags(filter: $filter, tag_filter: $tag_filter) {
|
||||
query FindTags(
|
||||
$filter: FindFilterType
|
||||
$tag_filter: TagFilterType
|
||||
$ids: [ID!]
|
||||
) {
|
||||
findTags(filter: $filter, tag_filter: $tag_filter, ids: $ids) {
|
||||
count
|
||||
tags {
|
||||
...TagData
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useToast } from "src/hooks/Toast";
|
|||
import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ScrapeDialog } from "../Shared/ScrapeDialog/ScrapeDialog";
|
||||
import {
|
||||
ScrapedCustomFieldRows,
|
||||
ScrapedImageRow,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedStringListRow,
|
||||
|
|
@ -27,9 +28,9 @@ import {
|
|||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { sortStoredIdObjects } from "src/utils/data";
|
||||
import {
|
||||
CustomFieldScrapeResults,
|
||||
ObjectListScrapeResult,
|
||||
ScrapeResult,
|
||||
ZeroableScrapeResult,
|
||||
hasScrapedValues,
|
||||
} from "../Shared/ScrapeDialog/scrapeResult";
|
||||
import { ScrapedTagsRow } from "../Shared/ScrapeDialog/ScrapedObjectsRow";
|
||||
|
|
@ -40,39 +41,6 @@ import {
|
|||
import { PerformerSelect } from "./PerformerSelect";
|
||||
import { uniq } from "lodash-es";
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
type CustomFieldScrapeResults = Map<string, ZeroableScrapeResult<any>>;
|
||||
|
||||
// There are a bunch of similar functions in PerformerScrapeDialog, but since we don't support
|
||||
// scraping custom fields, this one is only needed here. The `renderScraped` naming is kept the same
|
||||
// for consistency.
|
||||
function renderScrapedCustomFieldRows(
|
||||
results: CustomFieldScrapeResults,
|
||||
onChange: (newCustomFields: CustomFieldScrapeResults) => void
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{Array.from(results.entries()).map(([field, result]) => {
|
||||
const fieldName = `custom_${field}`;
|
||||
return (
|
||||
<ScrapedInputGroupRow
|
||||
className="custom-field"
|
||||
title={field}
|
||||
field={fieldName}
|
||||
key={fieldName}
|
||||
result={result}
|
||||
onChange={(newResult) => {
|
||||
const newResults = new Map(results);
|
||||
newResults.set(field, newResult);
|
||||
onChange(newResults);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type MergeOptions = {
|
||||
values: GQL.PerformerUpdateInput;
|
||||
};
|
||||
|
|
@ -604,10 +572,12 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
|
|||
result={image}
|
||||
onChange={(value) => setImage(value)}
|
||||
/>
|
||||
{hasCustomFieldValues &&
|
||||
renderScrapedCustomFieldRows(customFields, (newCustomFields) =>
|
||||
setCustomFields(newCustomFields)
|
||||
)}
|
||||
{hasCustomFieldValues && (
|
||||
<ScrapedCustomFieldRows
|
||||
results={customFields}
|
||||
onChange={(newCustomFields) => setCustomFields(newCustomFields)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { getCountryByISO } from "src/utils/country";
|
|||
import { CountrySelect } from "../CountrySelect";
|
||||
import { StringListInput } from "../StringListInput";
|
||||
import { ImageSelector } from "../ImageSelector";
|
||||
import { ScrapeResult } from "./scrapeResult";
|
||||
import { CustomFieldScrapeResults, ScrapeResult } from "./scrapeResult";
|
||||
import { ScrapeDialogContext } from "./ScrapeDialog";
|
||||
|
||||
function renderButtonIcon(selected: boolean) {
|
||||
|
|
@ -431,3 +431,30 @@ export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
|
|||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ScrapedCustomFieldRows: React.FC<{
|
||||
results: CustomFieldScrapeResults;
|
||||
onChange: (newCustomFields: CustomFieldScrapeResults) => void;
|
||||
}> = ({ results, onChange }) => {
|
||||
return (
|
||||
<>
|
||||
{Array.from(results.entries()).map(([field, result]) => {
|
||||
const fieldName = `custom_${field}`;
|
||||
return (
|
||||
<ScrapedInputGroupRow
|
||||
className="custom-field"
|
||||
title={field}
|
||||
field={fieldName}
|
||||
key={fieldName}
|
||||
result={result}
|
||||
onChange={(newResult) => {
|
||||
const newResults = new Map(results);
|
||||
newResults.set(field, newResult);
|
||||
onChange(newResults);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import lodashIsEqual from "lodash-es/isEqual";
|
|||
import clone from "lodash-es/clone";
|
||||
import { IHasStoredID } from "src/utils/data";
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export type CustomFieldScrapeResults = Map<string, ZeroableScrapeResult<any>>;
|
||||
|
||||
export class ScrapeResult<T> {
|
||||
public newValue?: T;
|
||||
public originalValue?: T;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,412 @@
|
|||
import { Button, Form, Col, Row } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
import { useTagsMerge } from "src/core/StashService";
|
||||
import { useIntl } from "react-intl";
|
||||
import { queryFindTagsByID, useTagsMerge } from "src/core/StashService";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Tag, TagSelect } from "./TagSelect";
|
||||
import {
|
||||
CustomFieldScrapeResults,
|
||||
hasScrapedValues,
|
||||
ObjectListScrapeResult,
|
||||
ScrapeResult,
|
||||
} from "../Shared/ScrapeDialog/scrapeResult";
|
||||
import { sortStoredIdObjects } from "src/utils/data";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { uniq } from "lodash-es";
|
||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import {
|
||||
ScrapedCustomFieldRows,
|
||||
ScrapeDialogRow,
|
||||
ScrapedImageRow,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedStringListRow,
|
||||
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)} />;
|
||||
};
|
||||
|
||||
interface ITagMergeDetailsProps {
|
||||
sources: GQL.TagDataFragment[];
|
||||
dest: GQL.TagDataFragment;
|
||||
onClose: (values?: GQL.TagUpdateInput) => void;
|
||||
}
|
||||
|
||||
const TagMergeDetails: React.FC<ITagMergeDetailsProps> = ({
|
||||
sources,
|
||||
dest,
|
||||
onClose,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const filterCandidates = useCallback(
|
||||
(t: { stored_id: string }) =>
|
||||
t.stored_id !== dest.id && sources.every((s) => s.id !== t.stored_id),
|
||||
[dest.id, sources]
|
||||
);
|
||||
|
||||
const [name, setName] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.name)
|
||||
);
|
||||
const [sortName, setSortName] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.sort_name)
|
||||
);
|
||||
const [aliases, setAliases] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(dest.aliases)
|
||||
);
|
||||
const [description, setDescription] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.description)
|
||||
);
|
||||
const [parentTags, setParentTags] = useState<
|
||||
ObjectListScrapeResult<GQL.ScrapedTag>
|
||||
>(
|
||||
new ObjectListScrapeResult<GQL.ScrapedTag>(
|
||||
sortStoredIdObjects(
|
||||
dest.parents.map(idToStoredID).filter(filterCandidates)
|
||||
)
|
||||
)
|
||||
);
|
||||
const [childTags, setChildTags] = useState<
|
||||
ObjectListScrapeResult<GQL.ScrapedTag>
|
||||
>(
|
||||
new ObjectListScrapeResult<GQL.ScrapedTag>(
|
||||
sortStoredIdObjects(
|
||||
dest.children.map(idToStoredID).filter(filterCandidates)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const [stashIDs, setStashIDs] = useState(new ScrapeResult<GQL.StashId[]>([]));
|
||||
|
||||
const [image, setImage] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(dest.image_path)
|
||||
);
|
||||
|
||||
const [customFields, setCustomFields] = useState<CustomFieldScrapeResults>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
function idToStoredID(o: { id: string; name: string }) {
|
||||
return {
|
||||
stored_id: o.id,
|
||||
name: o.name,
|
||||
};
|
||||
}
|
||||
|
||||
// calculate the values for everything
|
||||
// uses the first set value for single value fields, and combines all
|
||||
useEffect(() => {
|
||||
async function loadImages() {
|
||||
const src = sources.find((s) => s.image_path);
|
||||
if (!dest.image_path || !src) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const destData = await ImageUtils.imageToDataURL(dest.image_path);
|
||||
const srcData = await ImageUtils.imageToDataURL(src.image_path!);
|
||||
|
||||
// keep destination image by default
|
||||
const useNewValue = false;
|
||||
setImage(new ScrapeResult(destData, srcData, useNewValue));
|
||||
|
||||
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)
|
||||
);
|
||||
setSortName(
|
||||
new ScrapeResult(
|
||||
dest.sort_name,
|
||||
sources.find((s) => s.sort_name)?.sort_name,
|
||||
!dest.sort_name
|
||||
)
|
||||
);
|
||||
|
||||
setDescription(
|
||||
new ScrapeResult(
|
||||
dest.description,
|
||||
sources.find((s) => s.description)?.description,
|
||||
!dest.description
|
||||
)
|
||||
);
|
||||
|
||||
// default alias list should be the existing aliases, plus the names of all sources,
|
||||
// plus all source aliases, deduplicated
|
||||
const allAliases = uniq(
|
||||
dest.aliases.concat(
|
||||
sources.map((s) => s.name),
|
||||
sources.flatMap((s) => s.aliases)
|
||||
)
|
||||
);
|
||||
setAliases(new ScrapeResult(dest.aliases, allAliases, !!allAliases.length));
|
||||
|
||||
// default parent/child tags should be the existing tags, plus all source parent/child tags, deduplicated
|
||||
const allParentTags = uniq(all.flatMap((s) => s.parents))
|
||||
.map(idToStoredID)
|
||||
.filter(filterCandidates); // exclude self and sources
|
||||
|
||||
setParentTags(
|
||||
new ObjectListScrapeResult<GQL.ScrapedTag>(
|
||||
sortStoredIdObjects(dest.parents.map(idToStoredID)),
|
||||
sortStoredIdObjects(allParentTags),
|
||||
!!allParentTags.length
|
||||
)
|
||||
);
|
||||
|
||||
const allChildTags = uniq(all.flatMap((s) => s.children))
|
||||
.map(idToStoredID)
|
||||
.filter(filterCandidates); // exclude self and sources
|
||||
|
||||
setChildTags(
|
||||
new ObjectListScrapeResult<GQL.ScrapedTag>(
|
||||
sortStoredIdObjects(
|
||||
dest.children.map(idToStoredID).filter(filterCandidates)
|
||||
),
|
||||
sortStoredIdObjects(allChildTags),
|
||||
!!allChildTags.length
|
||||
)
|
||||
);
|
||||
|
||||
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,
|
||||
sources.find((s) => s.image_path)?.image_path,
|
||||
!dest.image_path
|
||||
)
|
||||
);
|
||||
|
||||
const customFieldNames = new Set<string>(Object.keys(dest.custom_fields));
|
||||
|
||||
for (const s of sources) {
|
||||
for (const n of Object.keys(s.custom_fields)) {
|
||||
customFieldNames.add(n);
|
||||
}
|
||||
}
|
||||
|
||||
setCustomFields(
|
||||
new Map(
|
||||
Array.from(customFieldNames)
|
||||
.sort()
|
||||
.map((field) => {
|
||||
return [
|
||||
field,
|
||||
new ScrapeResult(
|
||||
dest.custom_fields?.[field],
|
||||
sources.find((s) => s.custom_fields?.[field])?.custom_fields?.[
|
||||
field
|
||||
],
|
||||
dest.custom_fields?.[field] === undefined
|
||||
),
|
||||
];
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
loadImages();
|
||||
}, [sources, dest, filterCandidates]);
|
||||
|
||||
const hasCustomFieldValues = useMemo(() => {
|
||||
return hasScrapedValues(Array.from(customFields.values()));
|
||||
}, [customFields]);
|
||||
|
||||
// ensure this is updated if fields are changed
|
||||
const hasValues = useMemo(() => {
|
||||
return (
|
||||
hasCustomFieldValues ||
|
||||
hasScrapedValues([
|
||||
name,
|
||||
sortName,
|
||||
aliases,
|
||||
description,
|
||||
parentTags,
|
||||
childTags,
|
||||
stashIDs,
|
||||
image,
|
||||
])
|
||||
);
|
||||
}, [
|
||||
name,
|
||||
sortName,
|
||||
aliases,
|
||||
description,
|
||||
parentTags,
|
||||
childTags,
|
||||
stashIDs,
|
||||
image,
|
||||
hasCustomFieldValues,
|
||||
]);
|
||||
|
||||
function renderScrapeRows() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasValues) {
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage id="dialogs.merge.empty_results" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
field="name"
|
||||
title={intl.formatMessage({ id: "name" })}
|
||||
result={name}
|
||||
onChange={(value) => setName(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="sort_name"
|
||||
title={intl.formatMessage({ id: "sort_name" })}
|
||||
result={sortName}
|
||||
onChange={(value) => setSortName(value)}
|
||||
/>
|
||||
<ScrapedStringListRow
|
||||
field="aliases"
|
||||
title={intl.formatMessage({ id: "aliases" })}
|
||||
result={aliases}
|
||||
onChange={(value) => setAliases(value)}
|
||||
/>
|
||||
<ScrapedTagsRow
|
||||
field="parent_tags"
|
||||
title={intl.formatMessage({ id: "parent_tags" })}
|
||||
result={parentTags}
|
||||
onChange={(value) => setParentTags(value)}
|
||||
/>
|
||||
<ScrapedTagsRow
|
||||
field="child_tags"
|
||||
title={intl.formatMessage({ id: "sub_tags" })}
|
||||
result={childTags}
|
||||
onChange={(value) => setChildTags(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="description"
|
||||
title={intl.formatMessage({ id: "description" })}
|
||||
result={description}
|
||||
onChange={(value) => setDescription(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)}
|
||||
/>
|
||||
<ScrapedImageRow
|
||||
field="image"
|
||||
title={intl.formatMessage({ id: "tag_image" })}
|
||||
className="performer-image"
|
||||
result={image}
|
||||
onChange={(value) => setImage(value)}
|
||||
/>
|
||||
{hasCustomFieldValues && (
|
||||
<ScrapedCustomFieldRows
|
||||
results={customFields}
|
||||
onChange={(newCustomFields) => setCustomFields(newCustomFields)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function createValues(): GQL.TagUpdateInput {
|
||||
// only set the cover image if it's different from the existing cover image
|
||||
const coverImage = image.useNewValue ? image.getNewValue() : undefined;
|
||||
|
||||
return {
|
||||
id: dest.id,
|
||||
name: name.getNewValue(),
|
||||
sort_name: sortName.getNewValue(),
|
||||
aliases: aliases
|
||||
.getNewValue()
|
||||
?.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0),
|
||||
parent_ids: parentTags.getNewValue()?.map((t) => t.stored_id!),
|
||||
child_ids: childTags.getNewValue()?.map((t) => t.stored_id!),
|
||||
description: description.getNewValue(),
|
||||
stash_ids: stashIDs.getNewValue(),
|
||||
image: coverImage,
|
||||
custom_fields: {
|
||||
partial: Object.fromEntries(
|
||||
Array.from(customFields.entries()).flatMap(([field, v]) =>
|
||||
v.useNewValue ? [[field, v.getNewValue()]] : []
|
||||
)
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dialogTitle = intl.formatMessage({
|
||||
id: "actions.merge",
|
||||
});
|
||||
|
||||
const destinationLabel = !hasValues
|
||||
? ""
|
||||
: intl.formatMessage({ id: "dialogs.merge.destination" });
|
||||
const sourceLabel = !hasValues
|
||||
? ""
|
||||
: intl.formatMessage({ id: "dialogs.merge.source" });
|
||||
|
||||
return (
|
||||
<ScrapeDialog
|
||||
className="tag-merge-dialog"
|
||||
title={dialogTitle}
|
||||
existingLabel={destinationLabel}
|
||||
scrapedLabel={sourceLabel}
|
||||
onClose={(apply) => {
|
||||
if (!apply) {
|
||||
onClose();
|
||||
} else {
|
||||
onClose(createValues());
|
||||
}
|
||||
}}
|
||||
>
|
||||
{renderScrapeRows()}
|
||||
</ScrapeDialog>
|
||||
);
|
||||
};
|
||||
|
||||
interface ITagMergeModalProps {
|
||||
show: boolean;
|
||||
|
|
@ -23,6 +422,11 @@ export const TagMergeModal: React.FC<ITagMergeModalProps> = ({
|
|||
const [src, setSrc] = useState<Tag[]>([]);
|
||||
const [dest, setDest] = useState<Tag | null>(null);
|
||||
|
||||
const [loadedSources, setLoadedSources] = useState<GQL.TagDataFragment[]>([]);
|
||||
const [loadedDest, setLoadedDest] = useState<GQL.TagDataFragment>();
|
||||
|
||||
const [secondStep, setSecondStep] = useState(false);
|
||||
|
||||
const [running, setRunning] = useState(false);
|
||||
|
||||
const [mergeTags] = useTagsMerge();
|
||||
|
|
@ -41,7 +445,18 @@ export const TagMergeModal: React.FC<ITagMergeModalProps> = ({
|
|||
}
|
||||
}, [tags]);
|
||||
|
||||
async function onMerge() {
|
||||
async function loadTags() {
|
||||
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);
|
||||
}
|
||||
|
||||
async function onMerge(values: GQL.TagUpdateInput) {
|
||||
if (!dest) return;
|
||||
|
||||
const source = src.map((s) => s.id);
|
||||
|
|
@ -53,6 +468,7 @@ export const TagMergeModal: React.FC<ITagMergeModalProps> = ({
|
|||
variables: {
|
||||
source,
|
||||
destination,
|
||||
values,
|
||||
},
|
||||
});
|
||||
if (result.data?.tagsMerge) {
|
||||
|
|
@ -78,6 +494,23 @@ export const TagMergeModal: React.FC<ITagMergeModalProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
if (secondStep && dest) {
|
||||
return (
|
||||
<TagMergeDetails
|
||||
sources={loadedSources}
|
||||
dest={loadedDest!}
|
||||
onClose={(values) => {
|
||||
setSecondStep(false);
|
||||
if (values) {
|
||||
onMerge(values);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalComponent
|
||||
show={show}
|
||||
|
|
@ -85,7 +518,7 @@ export const TagMergeModal: React.FC<ITagMergeModalProps> = ({
|
|||
icon={faSignInAlt}
|
||||
accept={{
|
||||
text: intl.formatMessage({ id: "actions.merge" }),
|
||||
onClick: () => onMerge(),
|
||||
onClick: () => loadTags(),
|
||||
}}
|
||||
disabled={!canMerge()}
|
||||
cancel={{
|
||||
|
|
|
|||
|
|
@ -472,6 +472,14 @@ export const queryFindTagsForList = (filter: ListFilterModel) =>
|
|||
},
|
||||
});
|
||||
|
||||
export const queryFindTagsByID = (tagIDs: string[]) =>
|
||||
client.query<GQL.FindTagsQuery>({
|
||||
query: GQL.FindTagsDocument,
|
||||
variables: {
|
||||
ids: tagIDs,
|
||||
},
|
||||
});
|
||||
|
||||
export const queryFindTagsByIDForSelect = (tagIDs: string[]) =>
|
||||
client.query<GQL.FindTagsForSelectQuery>({
|
||||
query: GQL.FindTagsForSelectDocument,
|
||||
|
|
|
|||
Loading…
Reference in a new issue