diff --git a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx index 7c0364f8d78..58740eb47d0 100644 --- a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx +++ b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx @@ -1,159 +1,193 @@ -import React, { useState } from "react"; -import { useMutation, DocumentNode } from "@apollo/client"; -import { Button, Form } from "react-bootstrap"; +import React, { useEffect, useState } from "react"; +import { Form } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; +import { + mutateSubmitStashBoxPerformerDraft, + mutateSubmitStashBoxSceneDraft, +} from "src/core/StashService"; import { ModalComponent } from "src/components/Shared/Modal"; import { getStashboxBase } from "src/utils/stashbox"; import { FormattedMessage, useIntl } from "react-intl"; import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"; interface IProps { - show: boolean; - entity: { - name?: string | null; - id: string; - title?: string | null; - stash_ids: { stash_id: string; endpoint: string }[]; - }; + type: "scene" | "performer"; + entity: Pick< + GQL.SceneDataFragment | GQL.PerformerDataFragment, + "id" | "stash_ids" + >; boxes: Pick[]; - query: DocumentNode; + show: boolean; onHide: () => void; } -type Variables = - | GQL.SubmitStashBoxSceneDraftMutationVariables - | GQL.SubmitStashBoxPerformerDraftMutationVariables; -type Query = - | GQL.SubmitStashBoxSceneDraftMutation - | GQL.SubmitStashBoxPerformerDraftMutation; - -const isSceneDraft = ( - query: Query | null -): query is GQL.SubmitStashBoxSceneDraftMutation => - (query as GQL.SubmitStashBoxSceneDraftMutation).submitStashBoxSceneDraft !== - undefined; - -const getResponseId = (query: Query | null) => - isSceneDraft(query) - ? query.submitStashBoxSceneDraft - : query?.submitStashBoxPerformerDraft; - export const SubmitStashBoxDraft: React.FC = ({ - show, + type, boxes, entity, - query, + show, onHide, }) => { - const [submit, { data, error, loading }] = useMutation( - query - ); - const [selectedBoxIndex, setSelectedBoxIndex] = useState(0); const intl = useIntl(); - const handleSubmit = () => { - submit({ - variables: { - input: { - id: entity.id, - stash_box_index: selectedBoxIndex, - }, - }, - }); - }; + const [selectedBoxIndex, setSelectedBoxIndex] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const [reviewUrl, setReviewUrl] = useState(); - const selectedBox = - boxes.length > selectedBoxIndex ? boxes[selectedBoxIndex] : undefined; + // this can be undefined, if e.g. boxes is empty + // since we aren't using noUncheckedIndexedAccess, add undefined explicitly + const selectedBox: (typeof boxes)[number] | undefined = + boxes[selectedBoxIndex]; - const handleSelectBox = (e: React.ChangeEvent) => - setSelectedBoxIndex(Number.parseInt(e.currentTarget.value) ?? 0); + // #4354: reset state when shown, or if any props change + useEffect(() => { + if (show) { + setSelectedBoxIndex(0); + setLoading(false); + setError(undefined); + setReviewUrl(undefined); + } + }, [show, type, boxes, entity]); - if (!selectedBox) { - return <>; + async function doSubmit() { + if (type === "scene") { + const r = await mutateSubmitStashBoxSceneDraft({ + id: entity.id, + stash_box_index: selectedBoxIndex, + }); + return r.data?.submitStashBoxSceneDraft; + } else if (type === "performer") { + const r = await mutateSubmitStashBoxPerformerDraft({ + id: entity.id, + stash_box_index: selectedBoxIndex, + }); + return r.data?.submitStashBoxPerformerDraft; + } } - // If the scene has an attached stash_id from that endpoint, the operation will be an update - const isUpdate = - entity.stash_ids.find((id) => id.endpoint === selectedBox.endpoint) !== - undefined; + async function onSubmit() { + if (!selectedBox) return; - return ( - - {data === undefined ? ( + try { + setLoading(true); + const responseId = await doSubmit(); + + const stashboxBase = getStashboxBase(selectedBox.endpoint); + if (responseId) { + setReviewUrl(`${stashboxBase}drafts/${responseId}`); + } else { + // if the mutation returned a null id but didn't error, then just link to the drafts page + setReviewUrl(`${stashboxBase}drafts`); + } + } catch (e) { + if (e instanceof Error && e.message) { + setError(e.message); + } else { + setError(String(e)); + } + } finally { + setLoading(false); + } + } + + function renderContents() { + if (error !== undefined) { + return ( <> - - - : - - - {boxes.map((box, i) => ( - - ))} - - -
- {isUpdate && ( - - - - )} - -
+
+ +
+
{error}
- ) : ( + ); + } else if (reviewUrl !== undefined) { + return ( <>
- )} - {error !== undefined && ( - <> -
- -
-
{error.message}
- - )} + ); + } else { + return ( + + + : + + setSelectedBoxIndex(Number(e.currentTarget.value))} + value={selectedBoxIndex} + className="col-6 input-control" + > + {boxes.map((box, i) => ( + + ))} + + + ); + } + } + + function getFooterProps() { + if (error !== undefined || reviewUrl !== undefined) { + return { + accept: { + onClick: () => onHide(), + }, + }; + } + + // If the scene has an attached stash_id from that endpoint, the operation will be an update + const isUpdate = + entity.stash_ids.find((id) => id.endpoint === selectedBox?.endpoint) !== + undefined; + + return { + footerButtons: isUpdate && !loading && ( + + + + ), + accept: { + onClick: () => onSubmit(), + text: intl.formatMessage({ + id: isUpdate ? "actions.submit_update" : "actions.submit", + }), + variant: isUpdate ? "primary" : "success", + }, + cancel: { + onClick: () => onHide(), + variant: "secondary", + }, + }; + } + + return ( + + {renderContents()} ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerSubmitButton.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerSubmitButton.tsx index 479e5818409..c122999933d 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerSubmitButton.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerSubmitButton.tsx @@ -24,9 +24,9 @@ export const PerformerSubmitButton: React.FC = ({ setShowDraftModal(false)} /> diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 7c0b5695ec4..15465abd5eb 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -531,9 +531,9 @@ const ScenePage: React.FC = ({ setShowDraftModal(false)} /> diff --git a/ui/v2.5/src/components/Shared/Modal.tsx b/ui/v2.5/src/components/Shared/Modal.tsx index 08311565e49..09d58e38e46 100644 --- a/ui/v2.5/src/components/Shared/Modal.tsx +++ b/ui/v2.5/src/components/Shared/Modal.tsx @@ -1,12 +1,13 @@ import React from "react"; import { Button, Modal, Spinner, ModalProps } from "react-bootstrap"; +import { ButtonVariant } from "react-bootstrap/types"; import { Icon } from "./Icon"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { FormattedMessage } from "react-intl"; interface IButton { text?: string; - variant?: "danger" | "primary" | "secondary"; + variant?: ButtonVariant; onClick?: () => void; } diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 8e4fe65ce5b..d32982f492e 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -1945,6 +1945,22 @@ export const queryScrapeGalleryURL = (url: string) => fetchPolicy: "network-only", }); +export const mutateSubmitStashBoxSceneDraft = ( + input: GQL.StashBoxDraftSubmissionInput +) => + client.mutate({ + mutation: GQL.SubmitStashBoxSceneDraftDocument, + variables: { input }, + }); + +export const mutateSubmitStashBoxPerformerDraft = ( + input: GQL.StashBoxDraftSubmissionInput +) => + client.mutate({ + mutation: GQL.SubmitStashBoxPerformerDraftDocument, + variables: { input }, + }); + /// Packages export const useInstalledScraperPackages = GQL.useInstalledScraperPackagesQuery; export const useInstalledScraperPackagesStatus =