mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Feature: Manual StashId Search - Tags (#6374)
This commit is contained in:
parent
877491e62b
commit
39fd8a6550
11 changed files with 360 additions and 50 deletions
|
|
@ -165,6 +165,12 @@ type Query {
|
||||||
input: ScrapeSingleStudioInput!
|
input: ScrapeSingleStudioInput!
|
||||||
): [ScrapedStudio!]!
|
): [ScrapedStudio!]!
|
||||||
|
|
||||||
|
"Scrape for a single tag"
|
||||||
|
scrapeSingleTag(
|
||||||
|
source: ScraperSourceInput!
|
||||||
|
input: ScrapeSingleTagInput!
|
||||||
|
): [ScrapedTag!]!
|
||||||
|
|
||||||
"Scrape for a single performer"
|
"Scrape for a single performer"
|
||||||
scrapeSinglePerformer(
|
scrapeSinglePerformer(
|
||||||
source: ScraperSourceInput!
|
source: ScraperSourceInput!
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,13 @@ input ScrapeSingleStudioInput {
|
||||||
query: String
|
query: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input ScrapeSingleTagInput {
|
||||||
|
"""
|
||||||
|
Query can be either a name or a Stash ID
|
||||||
|
"""
|
||||||
|
query: String
|
||||||
|
}
|
||||||
|
|
||||||
input ScrapeSinglePerformerInput {
|
input ScrapeSinglePerformerInput {
|
||||||
"Instructs to query by string"
|
"Instructs to query by string"
|
||||||
query: String
|
query: String
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,21 @@ query FindStudio($id: ID, $name: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query FindTag($id: ID, $name: String) {
|
||||||
|
findTag(id: $id, name: $name) {
|
||||||
|
...TagFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query QueryTags($input: TagQueryInput!) {
|
||||||
|
queryTags(input: $input) {
|
||||||
|
count
|
||||||
|
tags {
|
||||||
|
...TagFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutation SubmitFingerprint($input: FingerprintSubmission!) {
|
mutation SubmitFingerprint($input: FingerprintSubmission!) {
|
||||||
submitFingerprint(input: $input)
|
submitFingerprint(input: $input)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,45 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
|
||||||
return nil, errors.New("stash_box_endpoint must be set")
|
return nil, errors.New("stash_box_endpoint must be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) ScrapeSingleTag(ctx context.Context, source scraper.Source, input ScrapeSingleTagInput) ([]*models.ScrapedTag, error) {
|
||||||
|
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||||
|
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := r.newStashBoxClient(*b)
|
||||||
|
|
||||||
|
var ret []*models.ScrapedTag
|
||||||
|
out, err := client.QueryTag(ctx, *input.Query)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if out != nil {
|
||||||
|
ret = append(ret, out...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ret) > 0 {
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
for _, tag := range ret {
|
||||||
|
if err := match.ScrapedTag(ctx, r.repository.Tag, tag, b.Endpoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("stash_box_endpoint must be set")
|
||||||
|
}
|
||||||
|
|
||||||
func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scraper.Source, input ScrapeSinglePerformerInput) ([]*models.ScrapedPerformer, error) {
|
func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scraper.Source, input ScrapeSinglePerformerInput) ([]*models.ScrapedPerformer, error) {
|
||||||
var ret []*models.ScrapedPerformer
|
var ret []*models.ScrapedPerformer
|
||||||
switch {
|
switch {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ type StashBoxGraphQLClient interface {
|
||||||
FindSceneByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindSceneByID, error)
|
FindSceneByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindSceneByID, error)
|
||||||
FindStudio(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindStudio, error)
|
FindStudio(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindStudio, error)
|
||||||
FindTag(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindTag, error)
|
FindTag(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindTag, error)
|
||||||
|
QueryTags(ctx context.Context, input TagQueryInput, interceptors ...clientv2.RequestInterceptor) (*QueryTags, error)
|
||||||
SubmitFingerprint(ctx context.Context, input FingerprintSubmission, interceptors ...clientv2.RequestInterceptor) (*SubmitFingerprint, error)
|
SubmitFingerprint(ctx context.Context, input FingerprintSubmission, interceptors ...clientv2.RequestInterceptor) (*SubmitFingerprint, error)
|
||||||
Me(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*Me, error)
|
Me(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*Me, error)
|
||||||
SubmitSceneDraft(ctx context.Context, input SceneDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitSceneDraft, error)
|
SubmitSceneDraft(ctx context.Context, input SceneDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitSceneDraft, error)
|
||||||
|
|
@ -643,6 +644,24 @@ func (t *FindStudio_FindStudio_StudioFragment_Parent) GetName() string {
|
||||||
return t.Name
|
return t.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueryTags_QueryTags struct {
|
||||||
|
Count int "json:\"count\" graphql:\"count\""
|
||||||
|
Tags []*TagFragment "json:\"tags\" graphql:\"tags\""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *QueryTags_QueryTags) GetCount() int {
|
||||||
|
if t == nil {
|
||||||
|
t = &QueryTags_QueryTags{}
|
||||||
|
}
|
||||||
|
return t.Count
|
||||||
|
}
|
||||||
|
func (t *QueryTags_QueryTags) GetTags() []*TagFragment {
|
||||||
|
if t == nil {
|
||||||
|
t = &QueryTags_QueryTags{}
|
||||||
|
}
|
||||||
|
return t.Tags
|
||||||
|
}
|
||||||
|
|
||||||
type Me_Me struct {
|
type Me_Me struct {
|
||||||
Name string "json:\"name\" graphql:\"name\""
|
Name string "json:\"name\" graphql:\"name\""
|
||||||
}
|
}
|
||||||
|
|
@ -775,6 +794,17 @@ func (t *FindTag) GetFindTag() *TagFragment {
|
||||||
return t.FindTag
|
return t.FindTag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueryTags struct {
|
||||||
|
QueryTags QueryTags_QueryTags "json:\"queryTags\" graphql:\"queryTags\""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *QueryTags) GetQueryTags() *QueryTags_QueryTags {
|
||||||
|
if t == nil {
|
||||||
|
t = &QueryTags{}
|
||||||
|
}
|
||||||
|
return &t.QueryTags
|
||||||
|
}
|
||||||
|
|
||||||
type SubmitFingerprint struct {
|
type SubmitFingerprint struct {
|
||||||
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
|
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
|
||||||
}
|
}
|
||||||
|
|
@ -1736,6 +1766,37 @@ func (c *Client) FindTag(ctx context.Context, id *string, name *string, intercep
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QueryTagsDocument = `query QueryTags ($input: TagQueryInput!) {
|
||||||
|
queryTags(input: $input) {
|
||||||
|
count
|
||||||
|
tags {
|
||||||
|
... TagFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment TagFragment on Tag {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func (c *Client) QueryTags(ctx context.Context, input TagQueryInput, interceptors ...clientv2.RequestInterceptor) (*QueryTags, error) {
|
||||||
|
vars := map[string]any{
|
||||||
|
"input": input,
|
||||||
|
}
|
||||||
|
|
||||||
|
var res QueryTags
|
||||||
|
if err := c.Client.Post(ctx, "QueryTags", QueryTagsDocument, &res, vars, interceptors...); err != nil {
|
||||||
|
if c.Client.ParseDataWhenErrors {
|
||||||
|
return &res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
const SubmitFingerprintDocument = `mutation SubmitFingerprint ($input: FingerprintSubmission!) {
|
const SubmitFingerprintDocument = `mutation SubmitFingerprint ($input: FingerprintSubmission!) {
|
||||||
submitFingerprint(input: $input)
|
submitFingerprint(input: $input)
|
||||||
}
|
}
|
||||||
|
|
@ -1838,6 +1899,7 @@ var DocumentOperationNames = map[string]string{
|
||||||
FindSceneByIDDocument: "FindSceneByID",
|
FindSceneByIDDocument: "FindSceneByID",
|
||||||
FindStudioDocument: "FindStudio",
|
FindStudioDocument: "FindStudio",
|
||||||
FindTagDocument: "FindTag",
|
FindTagDocument: "FindTag",
|
||||||
|
QueryTagsDocument: "QueryTags",
|
||||||
SubmitFingerprintDocument: "SubmitFingerprint",
|
SubmitFingerprintDocument: "SubmitFingerprint",
|
||||||
MeDocument: "Me",
|
MeDocument: "Me",
|
||||||
SubmitSceneDraftDocument: "SubmitSceneDraft",
|
SubmitSceneDraftDocument: "SubmitSceneDraft",
|
||||||
|
|
|
||||||
67
pkg/stashbox/tag.go
Normal file
67
pkg/stashbox/tag.go
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
package stashbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/stashbox/graphql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryTag searches for tags by name or ID.
|
||||||
|
// If query is a valid UUID, it searches by ID (returns single result).
|
||||||
|
// Otherwise, it searches by name (returns multiple results).
|
||||||
|
func (c Client) QueryTag(ctx context.Context, query string) ([]*models.ScrapedTag, error) {
|
||||||
|
_, err := uuid.Parse(query)
|
||||||
|
if err == nil {
|
||||||
|
// Query is a UUID, use findTag for exact match
|
||||||
|
return c.findTagByID(ctx, query)
|
||||||
|
}
|
||||||
|
// Otherwise search by name
|
||||||
|
return c.queryTagsByName(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) findTagByID(ctx context.Context, id string) ([]*models.ScrapedTag, error) {
|
||||||
|
tag, err := c.client.FindTag(ctx, &id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag.FindTag == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*models.ScrapedTag{{
|
||||||
|
Name: tag.FindTag.Name,
|
||||||
|
RemoteSiteID: &tag.FindTag.ID,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) queryTagsByName(ctx context.Context, name string) ([]*models.ScrapedTag, error) {
|
||||||
|
input := graphql.TagQueryInput{
|
||||||
|
Name: &name,
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 25,
|
||||||
|
Direction: graphql.SortDirectionEnumAsc,
|
||||||
|
Sort: graphql.TagSortEnumName,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.client.QueryTags(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.QueryTags.Tags == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []*models.ScrapedTag
|
||||||
|
for _, t := range result.QueryTags.Tags {
|
||||||
|
ret = append(ret, &models.ScrapedTag{
|
||||||
|
Name: t.Name,
|
||||||
|
RemoteSiteID: &t.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,15 @@ query ScrapeSingleStudio(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query ScrapeSingleTag(
|
||||||
|
$source: ScraperSourceInput!
|
||||||
|
$input: ScrapeSingleTagInput!
|
||||||
|
) {
|
||||||
|
scrapeSingleTag(source: $source, input: $input) {
|
||||||
|
...ScrapedSceneTagData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query ScrapeSinglePerformer(
|
query ScrapeSinglePerformer(
|
||||||
$source: ScraperSourceInput!
|
$source: ScraperSourceInput!
|
||||||
$input: ScrapeSinglePerformerInput!
|
$input: ScrapeSinglePerformerInput!
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
stashBoxPerformerQuery,
|
stashBoxPerformerQuery,
|
||||||
stashBoxSceneQuery,
|
stashBoxSceneQuery,
|
||||||
stashBoxStudioQuery,
|
stashBoxStudioQuery,
|
||||||
|
stashBoxTagQuery,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { stringToGender } from "src/utils/gender";
|
import { stringToGender } from "src/utils/gender";
|
||||||
|
|
@ -22,9 +23,10 @@ import { stringToGender } from "src/utils/gender";
|
||||||
type SearchResultItem =
|
type SearchResultItem =
|
||||||
| GQL.ScrapedPerformerDataFragment
|
| GQL.ScrapedPerformerDataFragment
|
||||||
| GQL.ScrapedSceneDataFragment
|
| GQL.ScrapedSceneDataFragment
|
||||||
| GQL.ScrapedStudioDataFragment;
|
| GQL.ScrapedStudioDataFragment
|
||||||
|
| GQL.ScrapedSceneTagDataFragment;
|
||||||
|
|
||||||
export type StashBoxEntityType = "performer" | "scene" | "studio";
|
export type StashBoxEntityType = "performer" | "scene" | "studio" | "tag";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
entityType: StashBoxEntityType;
|
entityType: StashBoxEntityType;
|
||||||
|
|
@ -232,6 +234,27 @@ export const StudioSearchResult: React.FC<IStudioResultProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Tag Result Component
|
||||||
|
interface ITagResultProps {
|
||||||
|
tag: GQL.ScrapedSceneTagDataFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagSearchResult: React.FC<ITagResultProps> = ({ tag }) => {
|
||||||
|
return (
|
||||||
|
<div className="mt-3 search-item" style={{ cursor: "pointer" }}>
|
||||||
|
<div className="tag-result">
|
||||||
|
<Row>
|
||||||
|
<div className="col flex-column">
|
||||||
|
<h4 className="tag-name">
|
||||||
|
<span>{tag.name}</span>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Helper to get entity type message id for i18n
|
// Helper to get entity type message id for i18n
|
||||||
function getEntityTypeMessageId(entityType: StashBoxEntityType): string {
|
function getEntityTypeMessageId(entityType: StashBoxEntityType): string {
|
||||||
switch (entityType) {
|
switch (entityType) {
|
||||||
|
|
@ -241,6 +264,8 @@ function getEntityTypeMessageId(entityType: StashBoxEntityType): string {
|
||||||
return "scene";
|
return "scene";
|
||||||
case "studio":
|
case "studio":
|
||||||
return "studio";
|
return "studio";
|
||||||
|
case "tag":
|
||||||
|
return "tag";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,6 +278,8 @@ function getFoundMessageId(entityType: StashBoxEntityType): string {
|
||||||
return "dialogs.scenes_found";
|
return "dialogs.scenes_found";
|
||||||
case "studio":
|
case "studio":
|
||||||
return "dialogs.studios_found";
|
return "dialogs.studios_found";
|
||||||
|
case "tag":
|
||||||
|
return "dialogs.tags_found";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,6 +345,14 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
||||||
setResults(queryData.data?.scrapeSingleStudio ?? []);
|
setResults(queryData.data?.scrapeSingleStudio ?? []);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "tag": {
|
||||||
|
const queryData = await stashBoxTagQuery(
|
||||||
|
query,
|
||||||
|
selectedStashBox.endpoint
|
||||||
|
);
|
||||||
|
setResults(queryData.data?.scrapeSingleTag ?? []);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast.error(error);
|
Toast.error(error);
|
||||||
|
|
@ -357,6 +392,10 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
||||||
return (
|
return (
|
||||||
<StudioSearchResult studio={item as GQL.ScrapedStudioDataFragment} />
|
<StudioSearchResult studio={item as GQL.ScrapedStudioDataFragment} />
|
||||||
);
|
);
|
||||||
|
case "tag":
|
||||||
|
return (
|
||||||
|
<TagSearchResult tag={item as GQL.ScrapedSceneTagDataFragment} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
|
|
@ -11,11 +12,14 @@ import Mousetrap from "mousetrap";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
import { useConfigurationContext } from "src/hooks/Config";
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
||||||
import { getStashIDs } from "src/utils/stashIds";
|
import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
|
||||||
import { Tag, TagSelect } from "../TagSelect";
|
import { Tag, TagSelect } from "../TagSelect";
|
||||||
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
|
||||||
|
|
||||||
interface ITagEditPanel {
|
interface ITagEditPanel {
|
||||||
tag: Partial<GQL.TagDataFragment>;
|
tag: Partial<GQL.TagDataFragment>;
|
||||||
|
|
@ -36,9 +40,13 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const { configuration: stashConfig } = useConfigurationContext();
|
||||||
|
|
||||||
const isNew = tag.id === undefined;
|
const isNew = tag.id === undefined;
|
||||||
|
|
||||||
|
// Editing state
|
||||||
|
const [isStashIDSearchOpen, setIsStashIDSearchOpen] = useState(false);
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
|
@ -143,6 +151,14 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onStashIDSelected(item?: GQL.StashIdInput) {
|
||||||
|
if (!item) return;
|
||||||
|
formik.setFieldValue(
|
||||||
|
"stash_ids",
|
||||||
|
addUpdateStashID(formik.values.stash_ids, item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
renderField,
|
renderField,
|
||||||
renderInputField,
|
renderInputField,
|
||||||
|
|
@ -186,6 +202,21 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
|
|
||||||
// TODO: CSS class
|
// TODO: CSS class
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{isStashIDSearchOpen && (
|
||||||
|
<StashBoxIDSearchModal
|
||||||
|
entityType="tag"
|
||||||
|
stashBoxes={stashConfig?.general.stashBoxes ?? []}
|
||||||
|
excludedStashBoxEndpoints={formik.values.stash_ids.map(
|
||||||
|
(s) => s.endpoint
|
||||||
|
)}
|
||||||
|
onSelectItem={(item) => {
|
||||||
|
onStashIDSelected(item);
|
||||||
|
setIsStashIDSearchOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{isNew && (
|
{isNew && (
|
||||||
<h2>
|
<h2>
|
||||||
|
|
@ -215,7 +246,21 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
{renderInputField("description", "textarea")}
|
{renderInputField("description", "textarea")}
|
||||||
{renderParentTagsField()}
|
{renderParentTagsField()}
|
||||||
{renderSubTagsField()}
|
{renderSubTagsField()}
|
||||||
{renderStashIDsField("stash_ids", "tags")}
|
{renderStashIDsField(
|
||||||
|
"stash_ids",
|
||||||
|
"tags",
|
||||||
|
"stash_ids",
|
||||||
|
undefined,
|
||||||
|
<Button
|
||||||
|
variant="success"
|
||||||
|
className="mr-2 py-0"
|
||||||
|
onClick={() => setIsStashIDSearchOpen(true)}
|
||||||
|
disabled={!stashConfig?.general.stashBoxes?.length}
|
||||||
|
title={intl.formatMessage({ id: "actions.add_stash_id" })}
|
||||||
|
>
|
||||||
|
<Icon icon={faPlus} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<hr />
|
<hr />
|
||||||
{renderInputField("ignore_auto_tag", "checkbox")}
|
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
@ -227,7 +272,9 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
isEditing
|
isEditing
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
onSave={formik.handleSubmit}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
saveDisabled={
|
||||||
|
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||||
|
}
|
||||||
onImageChange={onImageChange}
|
onImageChange={onImageChange}
|
||||||
onImageChangeURL={onImageLoad}
|
onImageChangeURL={onImageLoad}
|
||||||
onClearImage={() => onImageLoad(null)}
|
onClearImage={() => onImageLoad(null)}
|
||||||
|
|
@ -235,5 +282,6 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
acceptSVG
|
acceptSVG
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2329,6 +2329,23 @@ export const stashBoxSceneQuery = (query: string, stashBoxEndpoint: string) =>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const stashBoxTagQuery = (
|
||||||
|
query: string | null,
|
||||||
|
stashBoxEndpoint: string
|
||||||
|
) =>
|
||||||
|
client.query<GQL.ScrapeSingleTagQuery, GQL.ScrapeSingleTagQueryVariables>({
|
||||||
|
query: GQL.ScrapeSingleTagDocument,
|
||||||
|
variables: {
|
||||||
|
source: {
|
||||||
|
stash_box_endpoint: stashBoxEndpoint,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
query: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
});
|
||||||
|
|
||||||
export const mutateStashBoxBatchPerformerTag = (
|
export const mutateStashBoxBatchPerformerTag = (
|
||||||
input: GQL.StashBoxBatchTagInput
|
input: GQL.StashBoxBatchTagInput
|
||||||
) =>
|
) =>
|
||||||
|
|
|
||||||
|
|
@ -1016,6 +1016,7 @@
|
||||||
},
|
},
|
||||||
"scenes_found": "{count} scenes found",
|
"scenes_found": "{count} scenes found",
|
||||||
"studios_found": "{count} studios found",
|
"studios_found": "{count} studios found",
|
||||||
|
"tags_found": "{count} tags found",
|
||||||
"scrape_entity_query": "{entity_type} Scrape Query",
|
"scrape_entity_query": "{entity_type} Scrape Query",
|
||||||
"scrape_entity_title": "{entity_type} Scrape Results",
|
"scrape_entity_title": "{entity_type} Scrape Results",
|
||||||
"scrape_results_existing": "Existing",
|
"scrape_results_existing": "Existing",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue