Show dialog when refreshing tags

This commit is contained in:
WithoutPants 2026-03-23 12:07:43 +11:00
parent 091ac26f88
commit 56cb04fcc4
3 changed files with 142 additions and 58 deletions

View file

@ -295,7 +295,8 @@
}
}
&-studio {
&-studio,
&-tag {
background-color: #495b68;
border-radius: 3px;
display: flex;
@ -303,7 +304,8 @@
max-width: 100%;
padding: 1rem;
.studio-card {
.studio-card,
.tag-card {
box-shadow: none;
flex-shrink: 0;
margin: 0;

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
@ -47,6 +47,10 @@ const TagModal: React.FC<ITagModalProps> = ({
!!tag.parent && !tag.parent.stored_id
);
useEffect(() => {
setCreateParentTag(!excluded.parent_ids && !!tag.parent);
}, [excluded.parent_ids, tag.parent]);
// Check if a tag with the parent name already exists locally.
// Categories don't have stash IDs, so stored_id may be null even when the
// parent tag has already been created (e.g. by tagging a sibling tag first).
@ -70,6 +74,7 @@ const TagModal: React.FC<ITagModalProps> = ({
const [parentExcluded, setParentExcluded] = useState<Record<string, boolean>>(
excludedTagFields.reduce((dict, field) => ({ ...dict, [field]: true }), {})
);
const toggleParentField = (name: string) =>
setParentExcluded({
...parentExcluded,
@ -79,9 +84,11 @@ const TagModal: React.FC<ITagModalProps> = ({
function maybeRenderField(
id: string,
text: string | null | undefined,
isSelectable: boolean = true
isSelectable: boolean = true,
messageId?: string
) {
if (!text) return;
if (!messageId) messageId = id;
return (
<div className="row no-gutters">
@ -96,7 +103,7 @@ const TagModal: React.FC<ITagModalProps> = ({
</Button>
)}
<strong>
<FormattedMessage id={id} />:
<FormattedMessage id={messageId} />:
</strong>
</div>
<TruncatedText className="col-7" text={text} lineCount={3} />
@ -159,10 +166,18 @@ const TagModal: React.FC<ITagModalProps> = ({
function maybeRenderParentTag() {
// No parent tag, or parent already exists locally
if (!tag.parent || tag.parent.stored_id || !sendParentTag) {
if (
!tag.parent ||
tag.parent.stored_id ||
!sendParentTag ||
excluded.parent_ids
) {
return;
}
// force create if there is no current parent tag and parent tag is not excluded
const mustCreateParent = true;
return (
<div>
<div className="mb-4 mt-4">
@ -172,6 +187,7 @@ const TagModal: React.FC<ITagModalProps> = ({
label={intl.formatMessage({
id: "actions.create_parent_tag",
})}
disabled={mustCreateParent}
onChange={() => setCreateParentTag(!createParentTag)}
/>
</div>
@ -251,7 +267,12 @@ const TagModal: React.FC<ITagModalProps> = ({
{maybeRenderField("name", tag.name)}
{maybeRenderField("description", tag.description)}
{maybeRenderField("aliases", tag.alias_list?.join(", "))}
{maybeRenderField("parent_tags", tag.parent?.name, false)}
{maybeRenderField(
"parent_ids",
tag.parent?.name,
true,
"parent_tags"
)}
{maybeRenderStashBoxLink()}
</div>
</div>

View file

@ -11,6 +11,7 @@ import {
useJobsSubscribe,
mutateStashBoxBatchTagTag,
getClient,
useTagCreate,
} from "src/core/StashService";
import { useConfigurationContext } from "src/hooks/Config";
@ -27,6 +28,10 @@ import {
BatchAddModal,
} from "src/components/Shared/BatchModals";
import { StashBoxSelectorField } from "../StashBoxSelector";
import { apolloError } from "src/utils";
import TagModal from "./TagModal";
import { faTags } from "@fortawesome/free-solid-svg-icons";
import { uniq } from "lodash-es";
type JobFragment = Pick<
GQL.Job,
@ -59,6 +64,7 @@ const TagTaggerList: React.FC<ITagTaggerListProps> = ({
const intl = useIntl();
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState<
Record<string, GQL.ScrapedSceneTagDataFragment[]>
>({});
@ -94,6 +100,13 @@ const TagTaggerList: React.FC<ITagTaggerListProps> = ({
},
});
const [modalTag, setModalTag] = useState<
| {
existingTag: GQL.TagListDataFragment;
scrapedTag: GQL.ScrapedSceneTagDataFragment;
}
| undefined
>();
const [error, setError] = useState<
Record<string, { message?: string; details?: string } | undefined>
>({});
@ -128,64 +141,30 @@ const TagTaggerList: React.FC<ITagTaggerListProps> = ({
setLoading(true);
};
const [createTag] = useTagCreate();
const updateTag = useUpdateTag();
const doBoxUpdate = (tagID: string, stashID: string, endpoint: string) => {
const doBoxUpdate = (
tag: GQL.TagListDataFragment,
stashID: string,
endpoint: string
) => {
setLoadingUpdate(stashID);
setError({
...error,
[tagID]: undefined,
[tag.id]: undefined,
});
stashBoxTagQuery(stashID, endpoint)
.then(async (queryData) => {
const data = queryData.data?.scrapeSingleTag ?? [];
if (data.length > 0) {
const stashboxTag = data[0];
const updateData: GQL.TagUpdateInput = {
id: tagID,
};
if (
!(config.excludedTagFields ?? []).includes("name") &&
stashboxTag.name
) {
updateData.name = stashboxTag.name;
}
if (
stashboxTag.description &&
!(config.excludedTagFields ?? []).includes("description")
) {
updateData.description = stashboxTag.description;
}
if (
stashboxTag.alias_list &&
stashboxTag.alias_list.length > 0 &&
!(config.excludedTagFields ?? []).includes("aliases")
) {
updateData.aliases = stashboxTag.alias_list;
}
if (stashboxTag.remote_site_id) {
updateData.stash_ids = await mergeTagStashIDs(tagID, [
{
endpoint,
stash_id: stashboxTag.remote_site_id,
},
]);
}
const res = await updateTag(updateData);
if (!res?.data?.tagUpdate) {
setError({
...error,
[tagID]: {
message: `Failed to update tag`,
details: res?.errors?.[0]?.message ?? "",
},
});
}
setModalTag({
scrapedTag: {
...data[0],
stored_id: tag.id,
},
existingTag: tag,
});
}
})
.finally(() => setLoadingUpdate(undefined));
@ -205,6 +184,75 @@ const TagTaggerList: React.FC<ITagTaggerListProps> = ({
setShowBatchUpdate(false);
};
function handleSaveError(tagID: string, name: string, message: string) {
setError({
...error,
[tagID]: {
message: intl.formatMessage(
{ id: "tag_tagger.failed_to_save_tag" },
{ tag: name }
),
details:
message === "UNIQUE constraint failed: tags.name"
? intl.formatMessage({
id: "tag_tagger.name_already_exists",
})
: message,
},
});
}
const handleTagUpdate = async (
input: GQL.TagCreateInput,
parentInput?: GQL.TagCreateInput
) => {
const { existingTag, scrapedTag: tag } = modalTag!;
const tagID = existingTag.id;
setModalTag(undefined);
if (tagID) {
if (parentInput) {
try {
// cannot update parent tags, since there may be many
if (!!input.parent_ids?.length) {
// ignore
} else {
const parentRes = await createTag({
variables: { input: parentInput },
});
const parentID = parentRes.data?.tagCreate?.id;
if (parentID) {
// merge parent ids below
input.parent_ids = [parentID];
}
}
} catch (e) {
handleSaveError(tagID, parentInput.name, apolloError(e));
}
}
// always merge parent ids if included
if (input.parent_ids) {
input.parent_ids = uniq(
existingTag.parents.map((p) => p.id).concat(input.parent_ids)
);
}
const updateData: GQL.TagUpdateInput = {
...input,
id: tagID,
};
updateData.stash_ids = await mergeTagStashIDs(
tagID,
input.stash_ids ?? []
);
const res = await updateTag(updateData);
if (!res?.data?.tagUpdate)
handleSaveError(tagID, tag.name ?? "", res?.errors?.[0]?.message ?? "");
}
};
const handleTaggedTag = (
tag: Pick<GQL.TagListDataFragment, "id"> &
Partial<Omit<GQL.TagListDataFragment, "id">>
@ -292,7 +340,7 @@ const TagTaggerList: React.FC<ITagTaggerListProps> = ({
<InputGroup.Append>
<Button
onClick={() =>
doBoxUpdate(tag.id, stashID.stash_id, stashID.endpoint)
doBoxUpdate(tag, stashID.stash_id, stashID.endpoint)
}
disabled={!!loadingUpdate}
>
@ -344,11 +392,11 @@ const TagTaggerList: React.FC<ITagTaggerListProps> = ({
}
return (
<div key={tag.id} className={`${CLASSNAME}-studio`}>
<div key={tag.id} className={`${CLASSNAME}-tag`}>
<div className={`${CLASSNAME}-details`}>
<div></div>
<div>
<Card className="studio-card">
<Card className="tag-card">
<img loading="lazy" src={tag.image_path ?? ""} alt="" />
</Card>
</div>
@ -395,6 +443,19 @@ const TagTaggerList: React.FC<ITagTaggerListProps> = ({
entityName="tag"
/>
)}
{modalTag && (
<TagModal
closeModal={() => setModalTag(undefined)}
modalVisible={modalTag !== undefined}
tag={modalTag.scrapedTag}
onSave={handleTagUpdate}
icon={faTags}
header="Update Tag"
excludedTagFields={config.excludedTagFields}
endpoint={selectedEndpoint.endpoint}
/>
)}
<div className="ml-auto mb-3">
<Button onClick={() => setShowBatchAdd(true)}>
<FormattedMessage id="tag_tagger.batch_add_tags" />