Manually Search Stash ID - Edit Page - Scenes, Studios (#6340)

This commit is contained in:
Gykes
2025-12-03 16:09:49 -06:00
committed by GitHub
parent 3d044896ad
commit 877491e62b
8 changed files with 339 additions and 38 deletions

View File

@@ -350,7 +350,7 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
return nil, nil return nil, nil
} }
return nil, errors.New("stash_box_index must be set") 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) {

View File

@@ -17,6 +17,7 @@ type StashBoxGraphQLClient interface {
FindPerformerByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindPerformerByID, error) FindPerformerByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindPerformerByID, error)
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)
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)
@@ -763,6 +764,17 @@ func (t *FindStudio) GetFindStudio() *StudioFragment {
return t.FindStudio return t.FindStudio
} }
type FindTag struct {
FindTag *TagFragment "json:\"findTag,omitempty\" graphql:\"findTag\""
}
func (t *FindTag) GetFindTag() *TagFragment {
if t == nil {
t = &FindTag{}
}
return t.FindTag
}
type SubmitFingerprint struct { type SubmitFingerprint struct {
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
} }
@@ -1695,6 +1707,35 @@ func (c *Client) FindStudio(ctx context.Context, id *string, name *string, inter
return &res, nil return &res, nil
} }
const FindTagDocument = `query FindTag ($id: ID, $name: String) {
findTag(id: $id, name: $name) {
... TagFragment
}
}
fragment TagFragment on Tag {
name
id
}
`
func (c *Client) FindTag(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindTag, error) {
vars := map[string]any{
"id": id,
"name": name,
}
var res FindTag
if err := c.Client.Post(ctx, "FindTag", FindTagDocument, &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)
} }
@@ -1796,6 +1837,7 @@ var DocumentOperationNames = map[string]string{
FindPerformerByIDDocument: "FindPerformerByID", FindPerformerByIDDocument: "FindPerformerByID",
FindSceneByIDDocument: "FindSceneByID", FindSceneByIDDocument: "FindSceneByID",
FindStudioDocument: "FindStudio", FindStudioDocument: "FindStudio",
FindTagDocument: "FindTag",
SubmitFingerprintDocument: "SubmitFingerprint", SubmitFingerprintDocument: "SubmitFingerprint",
MeDocument: "Me", MeDocument: "Me",
SubmitSceneDraftDocument: "SubmitSceneDraft", SubmitSceneDraftDocument: "SubmitSceneDraft",

View File

@@ -15,7 +15,7 @@ import { ImageInput } from "src/components/Shared/ImageInput";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { CountrySelect } from "src/components/Shared/CountrySelect"; import { CountrySelect } from "src/components/Shared/CountrySelect";
import ImageUtils from "src/utils/image"; import ImageUtils from "src/utils/image";
import { getStashIDs } from "src/utils/stashIds"; import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
import { stashboxDisplayName } from "src/utils/stashbox"; import { stashboxDisplayName } from "src/utils/stashbox";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { Prompt } from "react-router-dom"; import { Prompt } from "react-router-dom";
@@ -574,23 +574,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
function onStashIDSelected(item?: GQL.StashIdInput) { function onStashIDSelected(item?: GQL.StashIdInput) {
if (!item) return; if (!item) return;
formik.setFieldValue(
// Check if StashID with this endpoint already exists "stash_ids",
const existingIndex = formik.values.stash_ids.findIndex( addUpdateStashID(formik.values.stash_ids, item)
(s) => s.endpoint === item.endpoint
); );
let newStashIDs;
if (existingIndex >= 0) {
// Replace existing StashID
newStashIDs = [...formik.values.stash_ids];
newStashIDs[existingIndex] = item;
} else {
// Add new StashID
newStashIDs = [...formik.values.stash_ids, item];
}
formik.setFieldValue("stash_ids", newStashIDs);
} }
function renderButtons(classNames: string) { function renderButtons(classNames: string) {
@@ -685,6 +672,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
{maybeRenderScrapeDialog()} {maybeRenderScrapeDialog()}
{isStashIDSearchOpen && ( {isStashIDSearchOpen && (
<StashBoxIDSearchModal <StashBoxIDSearchModal
entityType="performer"
stashBoxes={stashConfig?.general.stashBoxes ?? []} stashBoxes={stashConfig?.general.stashBoxes ?? []}
excludedStashBoxEndpoints={formik.values.stash_ids.map( excludedStashBoxEndpoints={formik.values.stash_ids.map(
(s) => s.endpoint (s) => s.endpoint

View File

@@ -16,12 +16,12 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { ImageInput } from "src/components/Shared/ImageInput"; import { ImageInput } from "src/components/Shared/ImageInput";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import ImageUtils from "src/utils/image"; import ImageUtils from "src/utils/image";
import { getStashIDs } from "src/utils/stashIds"; import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { Prompt } from "react-router-dom"; import { Prompt } from "react-router-dom";
import { useConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable"; import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable";
import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { faSearch, faPlus } from "@fortawesome/free-solid-svg-icons";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries"; import { galleryTitle } from "src/core/galleries";
import { lazyComponent } from "src/utils/lazyComponent"; import { lazyComponent } from "src/utils/lazyComponent";
@@ -41,6 +41,7 @@ import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
import { Group } from "src/components/Groups/GroupSelect"; import { Group } from "src/components/Groups/GroupSelect";
import { useTagsEdit } from "src/hooks/tagsEdit"; import { useTagsEdit } from "src/hooks/tagsEdit";
import { ScraperMenu } from "src/components/Shared/ScraperMenu"; import { ScraperMenu } from "src/components/Shared/ScraperMenu";
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog")); const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal")); const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
@@ -77,6 +78,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
const [scraper, setScraper] = useState<GQL.ScraperSourceInput>(); const [scraper, setScraper] = useState<GQL.ScraperSourceInput>();
const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] = const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] =
useState<boolean>(false); useState<boolean>(false);
const [isStashIDSearchOpen, setIsStashIDSearchOpen] =
useState<boolean>(false);
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>(); const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
const [endpoint, setEndpoint] = useState<string>(); const [endpoint, setEndpoint] = useState<string>();
@@ -547,6 +550,14 @@ export const SceneEditPanel: React.FC<IProps> = ({
} }
} }
function onStashIDSelected(item?: GQL.StashIdInput) {
if (!item) return;
formik.setFieldValue(
"stash_ids",
addUpdateStashID(formik.values.stash_ids, item)
);
}
const image = useMemo(() => { const image = useMemo(() => {
if (encodingImage) { if (encodingImage) {
return ( return (
@@ -696,6 +707,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
{renderScrapeQueryModal()} {renderScrapeQueryModal()}
{maybeRenderScrapeDialog()} {maybeRenderScrapeDialog()}
{isStashIDSearchOpen && (
<StashBoxIDSearchModal
entityType="scene"
stashBoxes={stashConfig?.general.stashBoxes ?? []}
excludedStashBoxEndpoints={formik.values.stash_ids.map(
(s) => s.endpoint
)}
onSelectItem={(item) => {
onStashIDSelected(item);
setIsStashIDSearchOpen(false);
}}
/>
)}
<Form noValidate onSubmit={formik.handleSubmit}> <Form noValidate onSubmit={formik.handleSubmit}>
<Row className="form-container edit-buttons-container px-3 pt-3"> <Row className="form-container edit-buttons-container px-3 pt-3">
<div className="edit-buttons mb-3 pl-0"> <div className="edit-buttons mb-3 pl-0">
@@ -761,7 +785,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
"stash_ids", "stash_ids",
"scenes", "scenes",
"stash_ids", "stash_ids",
fullWidthProps fullWidthProps,
<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>
)} )}
</Col> </Col>
<Col lg={5} xl={12}> <Col lg={5} xl={12}>

View File

@@ -11,11 +11,23 @@ import TextUtils from "src/utils/text";
import GenderIcon from "src/components/Performers/GenderIcon"; import GenderIcon from "src/components/Performers/GenderIcon";
import { CountryFlag } from "src/components/Shared/CountryFlag"; import { CountryFlag } from "src/components/Shared/CountryFlag";
import { Icon } from "src/components/Shared/Icon"; import { Icon } from "src/components/Shared/Icon";
import { stashBoxPerformerQuery } from "src/core/StashService"; import {
stashBoxPerformerQuery,
stashBoxSceneQuery,
stashBoxStudioQuery,
} 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";
type SearchResultItem =
| GQL.ScrapedPerformerDataFragment
| GQL.ScrapedSceneDataFragment
| GQL.ScrapedStudioDataFragment;
export type StashBoxEntityType = "performer" | "scene" | "studio";
interface IProps { interface IProps {
entityType: StashBoxEntityType;
stashBoxes: GQL.StashBox[]; stashBoxes: GQL.StashBox[];
excludedStashBoxEndpoints?: string[]; excludedStashBoxEndpoints?: string[];
onSelectItem: (item?: GQL.StashIdInput) => void; onSelectItem: (item?: GQL.StashIdInput) => void;
@@ -132,8 +144,121 @@ export const PerformerSearchResult: React.FC<IPerformerResultProps> = ({
); );
}; };
// Scene Result Component
interface ISceneResultProps {
scene: GQL.ScrapedSceneDataFragment;
}
const SceneSearchResultDetails: React.FC<ISceneResultProps> = ({ scene }) => {
return (
<div className="scene-result">
<Row>
<SearchResultImage imageUrl={scene.image} />
<div className="col flex-column">
<h4 className="scene-title">
<span>{scene.title}</span>
{scene.code && (
<span className="scene-code">{` (${scene.code})`}</span>
)}
</h4>
<h5 className="scene-details">
{scene.studio?.name && <span>{scene.studio.name}</span>}
{scene.date && (
<span className="scene-date">{`${scene.date}`}</span>
)}
</h5>
{scene.performers && scene.performers.length > 0 && (
<div className="scene-performers">
{scene.performers.map((p) => p.name).join(", ")}
</div>
)}
</div>
</Row>
<Row>
<Col>
<TruncatedText text={scene.details ?? ""} lineCount={3} />
</Col>
</Row>
<SearchResultTags tags={scene.tags} />
</div>
);
};
export const SceneSearchResult: React.FC<ISceneResultProps> = ({ scene }) => {
return (
<div className="mt-3 search-item" style={{ cursor: "pointer" }}>
<SceneSearchResultDetails scene={scene} />
</div>
);
};
// Studio Result Component
interface IStudioResultProps {
studio: GQL.ScrapedStudioDataFragment;
}
const StudioSearchResultDetails: React.FC<IStudioResultProps> = ({
studio,
}) => {
return (
<div className="studio-result">
<Row>
<SearchResultImage imageUrl={studio.image} />
<div className="col flex-column">
<h4 className="studio-name">
<span>{studio.name}</span>
</h4>
{studio.parent?.name && (
<h5 className="studio-parent">
<span>{studio.parent.name}</span>
</h5>
)}
{studio.urls && studio.urls.length > 0 && (
<div className="studio-url text-muted small">{studio.urls[0]}</div>
)}
</div>
</Row>
</div>
);
};
export const StudioSearchResult: React.FC<IStudioResultProps> = ({
studio,
}) => {
return (
<div className="mt-3 search-item" style={{ cursor: "pointer" }}>
<StudioSearchResultDetails studio={studio} />
</div>
);
};
// Helper to get entity type message id for i18n
function getEntityTypeMessageId(entityType: StashBoxEntityType): string {
switch (entityType) {
case "performer":
return "performer";
case "scene":
return "scene";
case "studio":
return "studio";
}
}
// Helper to get the "found" message id based on entity type
function getFoundMessageId(entityType: StashBoxEntityType): string {
switch (entityType) {
case "performer":
return "dialogs.performers_found";
case "scene":
return "dialogs.scenes_found";
case "studio":
return "dialogs.studios_found";
}
}
// Main Modal Component // Main Modal Component
export const StashBoxIDSearchModal: React.FC<IProps> = ({ export const StashBoxIDSearchModal: React.FC<IProps> = ({
entityType,
stashBoxes, stashBoxes,
excludedStashBoxEndpoints = [], excludedStashBoxEndpoints = [],
onSelectItem, onSelectItem,
@@ -146,9 +271,9 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
null null
); );
const [query, setQuery] = useState<string>(""); const [query, setQuery] = useState<string>("");
const [results, setResults] = useState< const [results, setResults] = useState<SearchResultItem[] | undefined>(
GQL.ScrapedPerformerDataFragment[] | undefined undefined
>(undefined); );
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -168,17 +293,38 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
setResults([]); setResults([]);
try { try {
const queryData = await stashBoxPerformerQuery( switch (entityType) {
query, case "performer": {
selectedStashBox.endpoint const queryData = await stashBoxPerformerQuery(
); query,
setResults(queryData.data?.scrapeSinglePerformer ?? []); selectedStashBox.endpoint
);
setResults(queryData.data?.scrapeSinglePerformer ?? []);
break;
}
case "scene": {
const queryData = await stashBoxSceneQuery(
query,
selectedStashBox.endpoint
);
setResults(queryData.data?.scrapeSingleScene ?? []);
break;
}
case "studio": {
const queryData = await stashBoxStudioQuery(
query,
selectedStashBox.endpoint
);
setResults(queryData.data?.scrapeSingleStudio ?? []);
break;
}
}
} catch (error) { } catch (error) {
Toast.error(error); Toast.error(error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [query, selectedStashBox, Toast]); }, [query, selectedStashBox, Toast, entityType]);
function handleItemClick(item: IHasRemoteSiteID) { function handleItemClick(item: IHasRemoteSiteID) {
if (selectedStashBox && item.remote_site_id) { if (selectedStashBox && item.remote_site_id) {
@@ -195,6 +341,25 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
onSelectItem(undefined); onSelectItem(undefined);
} }
function renderResultItem(item: SearchResultItem) {
switch (entityType) {
case "performer":
return (
<PerformerSearchResult
performer={item as GQL.ScrapedPerformerDataFragment}
/>
);
case "scene":
return (
<SceneSearchResult scene={item as GQL.ScrapedSceneDataFragment} />
);
case "studio":
return (
<StudioSearchResult studio={item as GQL.ScrapedStudioDataFragment} />
);
}
}
function renderResults() { function renderResults() {
if (!results || results.length === 0) { if (!results || results.length === 0) {
return null; return null;
@@ -204,14 +369,14 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
<div className={CLASSNAME_LIST_CONTAINER}> <div className={CLASSNAME_LIST_CONTAINER}>
<div className="mt-1 mb-2"> <div className="mt-1 mb-2">
<FormattedMessage <FormattedMessage
id="dialogs.performers_found" id={getFoundMessageId(entityType)}
values={{ count: results.length }} values={{ count: results.length }}
/> />
</div> </div>
<ul className={CLASSNAME_LIST} style={{ listStyleType: "none" }}> <ul className={CLASSNAME_LIST} style={{ listStyleType: "none" }}>
{results.map((item, i) => ( {results.map((item, i) => (
<li key={i} onClick={() => handleItemClick(item)}> <li key={i} onClick={() => handleItemClick(item)}>
<PerformerSearchResult performer={item} /> {renderResultItem(item)}
</li> </li>
))} ))}
</ul> </ul>
@@ -219,13 +384,17 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
); );
} }
const entityTypeDisplayName = intl.formatMessage({
id: getEntityTypeMessageId(entityType),
});
return ( return (
<ModalComponent <ModalComponent
show show
onHide={handleClose} onHide={handleClose}
header={intl.formatMessage( header={intl.formatMessage(
{ id: "stashbox_search.header" }, { id: "stashbox_search.header" },
{ entityType: "Performer" } { entityType: entityTypeDisplayName }
)} )}
accept={{ accept={{
text: intl.formatMessage({ id: "actions.cancel" }), text: intl.formatMessage({ id: "actions.cancel" }),
@@ -273,7 +442,7 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
value={query} value={query}
placeholder={intl.formatMessage( placeholder={intl.formatMessage(
{ id: "stashbox_search.placeholder_name_or_id" }, { id: "stashbox_search.placeholder_name_or_id" },
{ entityType: "Performer" } { entityType: entityTypeDisplayName }
)} )}
className="text-input" className="text-input"
ref={inputRef} ref={inputRef}

View File

@@ -5,18 +5,22 @@ import * as yup from "yup";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
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 { getStashIDs } from "src/utils/stashIds"; import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { Prompt } from "react-router-dom"; import { Prompt } from "react-router-dom";
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 { Studio, StudioSelect } from "../StudioSelect"; import { Studio, StudioSelect } from "../StudioSelect";
import { useTagsEdit } from "src/hooks/tagsEdit"; import { useTagsEdit } from "src/hooks/tagsEdit";
import { Icon } from "src/components/Shared/Icon";
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
interface IStudioEditPanel { interface IStudioEditPanel {
studio: Partial<GQL.StudioDataFragment>; studio: Partial<GQL.StudioDataFragment>;
@@ -37,9 +41,13 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const Toast = useToast(); const Toast = useToast();
const { configuration: stashConfig } = useConfigurationContext();
const isNew = studio.id === undefined; const isNew = studio.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 StudioEditPanel: React.FC<IStudioEditPanel> = ({
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,
@@ -173,6 +189,20 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
return ( return (
<> <>
{isStashIDSearchOpen && (
<StashBoxIDSearchModal
entityType="studio"
stashBoxes={stashConfig?.general.stashBoxes ?? []}
excludedStashBoxEndpoints={formik.values.stash_ids.map(
(s) => s.endpoint
)}
onSelectItem={(item) => {
onStashIDSelected(item);
setIsStashIDSearchOpen(false);
}}
/>
)}
<Prompt <Prompt
when={formik.dirty} when={formik.dirty}
message={(location, action) => { message={(location, action) => {
@@ -191,7 +221,21 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
{renderInputField("details", "textarea")} {renderInputField("details", "textarea")}
{renderParentStudioField()} {renderParentStudioField()}
{renderTagsField()} {renderTagsField()}
{renderStashIDsField("stash_ids", "studios")} {renderStashIDsField(
"stash_ids",
"studios",
"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>

View File

@@ -1015,6 +1015,7 @@
"video_previews_tooltip": "Video previews which play when hovering over a scene" "video_previews_tooltip": "Video previews which play when hovering over a scene"
}, },
"scenes_found": "{count} scenes found", "scenes_found": "{count} scenes found",
"studios_found": "{count} studios 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",

View File

@@ -1,3 +1,5 @@
import * as GQL from "src/core/generated-graphql";
export const getStashIDs = ( export const getStashIDs = (
ids?: { stash_id: string; endpoint: string; updated_at: string }[] ids?: { stash_id: string; endpoint: string; updated_at: string }[]
) => ) =>
@@ -32,3 +34,25 @@ export const separateNamesAndStashIds = (
return { names, stashIds }; return { names, stashIds };
}; };
/**
* Utility to add or update a StashID in an array.
* If a StashID with the same endpoint exists, it will be replaced.
* Otherwise, the new StashID will be appended.
*/
export const addUpdateStashID = (
existingStashIDs: GQL.StashIdInput[],
newItem: GQL.StashIdInput
): GQL.StashIdInput[] => {
const existingIndex = existingStashIDs.findIndex(
(s) => s.endpoint === newItem.endpoint
);
if (existingIndex >= 0) {
const newStashIDs = [...existingStashIDs];
newStashIDs[existingIndex] = newItem;
return newStashIDs;
}
return [...existingStashIDs, newItem];
};