diff --git a/ui/v2.5/src/components/Changelog/versions/v090.md b/ui/v2.5/src/components/Changelog/versions/v090.md index abbe3fe64..8855adad5 100644 --- a/ui/v2.5/src/components/Changelog/versions/v090.md +++ b/ui/v2.5/src/components/Changelog/versions/v090.md @@ -9,6 +9,7 @@ * Added not equals/greater than/less than modifiers for resolution criteria. ([#1568](https://github.com/stashapp/stash/pull/1568)) ### 🎨 Improvements +* Prompt when leaving gallery edit page with unsaved changes. ([#1654](https://github.com/stashapp/stash/pull/1654)) * Show largest duplicates first in scene duplicate checker. ([#1639](https://github.com/stashapp/stash/pull/1639)) * Added checkboxes to scene list view. ([#1642](https://github.com/stashapp/stash/pull/1642)) * Added keyboard shortcuts for scene queue navigation. ([#1635](https://github.com/stashapp/stash/pull/1635)) diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index b2e8092f4..c83b94bc7 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -11,6 +11,7 @@ import { } from "react-bootstrap"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; +import * as yup from "yup"; import { queryScrapeGallery, queryScrapeGalleryURL, @@ -28,7 +29,9 @@ import { LoadingIndicator, } from "src/components/Shared"; import { useToast } from "src/hooks"; -import { FormUtils, EditableTextUtils, TextUtils } from "src/utils"; +import { useFormik } from "formik"; +import { Prompt } from "react-router"; +import { FormUtils, TextUtils } from "src/utils"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; @@ -53,25 +56,11 @@ export const GalleryEditPanel: React.FC< const intl = useIntl(); const Toast = useToast(); const history = useHistory(); - const [title, setTitle] = useState(gallery?.title ?? ""); - const [details, setDetails] = useState(gallery?.details ?? ""); - const [url, setUrl] = useState(gallery?.url ?? ""); - const [date, setDate] = useState(gallery?.date ?? ""); - const [rating, setRating] = useState(gallery?.rating ?? NaN); - const [studioId, setStudioId] = useState( - gallery?.studio?.id ?? undefined - ); - const [performerIds, setPerformerIds] = useState( - gallery?.performers.map((p) => p.id) ?? [] - ); - const [tagIds, setTagIds] = useState( - gallery?.tags.map((t) => t.id) ?? [] - ); const [scenes, setScenes] = useState<{ id: string; title: string }[]>( - gallery?.scenes.map((s) => ({ + (gallery?.scenes ?? []).map((s) => ({ id: s.id, title: s.title ?? TextUtils.fileNameFromPath(s.path ?? ""), - })) ?? [] + })) ); const Scrapers = useListGalleryScrapers(); @@ -88,10 +77,59 @@ export const GalleryEditPanel: React.FC< const [createGallery] = useGalleryCreate(); const [updateGallery] = useGalleryUpdate(); + const schema = yup.object({ + title: yup.string().required(), + details: yup.string().optional().nullable(), + url: yup.string().optional().nullable(), + date: yup.string().optional().nullable(), + rating: yup.number().optional().nullable(), + studio_id: yup.string().optional().nullable(), + performer_ids: yup.array(yup.string().required()).optional().nullable(), + tag_ids: yup.array(yup.string().required()).optional().nullable(), + scene_ids: yup.array(yup.string().required()).optional().nullable(), + }); + + const initialValues = { + title: gallery?.title ?? "", + details: gallery?.details ?? "", + url: gallery?.url ?? "", + date: gallery?.date ?? "", + rating: gallery?.rating ?? null, + studio_id: gallery?.studio?.id, + performer_ids: (gallery?.performers ?? []).map((p) => p.id), + tag_ids: (gallery?.tags ?? []).map((t) => t.id), + scene_ids: (gallery?.scenes ?? []).map((s) => s.id), + }; + + type InputValues = typeof initialValues; + + const formik = useFormik({ + initialValues, + validationSchema: schema, + onSubmit: (values) => onSave(getGalleryInput(values)), + }); + + function setRating(v: number) { + formik.setFieldValue("rating", v); + } + + interface ISceneSelectValue { + id: string; + title: string; + } + + function onSetScenes(items: ISceneSelectValue[]) { + setScenes(items); + formik.setFieldValue( + "scene_ids", + items.map((i) => i.id) + ); + } + useEffect(() => { if (isVisible) { Mousetrap.bind("s s", () => { - onSave(); + formik.handleSubmit(); }); Mousetrap.bind("d d", () => { onDelete(); @@ -140,28 +178,24 @@ export const GalleryEditPanel: React.FC< setQueryableScrapers(newQueryableScrapers); }, [Scrapers]); - function getGalleryInput() { + function getGalleryInput( + input: InputValues + ): GQL.GalleryCreateInput | GQL.GalleryUpdateInput { return { id: isNew ? undefined : gallery?.id ?? "", - title: title ?? "", - details, - url, - date, - rating: rating ?? null, - studio_id: studioId ?? null, - performer_ids: performerIds, - tag_ids: tagIds, - scene_ids: scenes.map((s) => s.id), + ...input, }; } - async function onSave() { + async function onSave( + input: GQL.GalleryCreateInput | GQL.GalleryUpdateInput + ) { setIsLoading(true); try { if (isNew) { const result = await createGallery({ variables: { - input: getGalleryInput(), + input: input as GQL.GalleryCreateInput, }, }); if (result.data?.galleryCreate) { @@ -171,7 +205,7 @@ export const GalleryEditPanel: React.FC< } else { const result = await updateGallery({ variables: { - input: getGalleryInput() as GQL.GalleryUpdateInput, + input: input as GQL.GalleryUpdateInput, }, }); if (result.data?.galleryUpdate) { @@ -185,6 +219,7 @@ export const GalleryEditPanel: React.FC< } ), }); + formik.resetForm({ values: formik.values }); } } } catch (e) { @@ -196,7 +231,9 @@ export const GalleryEditPanel: React.FC< async function onScrapeClicked(scraper: GQL.Scraper) { setIsLoading(true); try { - const galleryInput = getGalleryInput() as GQL.GalleryUpdateInput; + const galleryInput = getGalleryInput( + formik.values + ) as GQL.GalleryUpdateInput; const result = await queryScrapeGallery(scraper.id, galleryInput); if (!result.data || !result.data.scrapeGallery) { Toast.success({ @@ -238,7 +275,7 @@ export const GalleryEditPanel: React.FC< return; } - const currentGallery = getGalleryInput(); + const currentGallery = getGalleryInput(formik.values); return ( 0) { const newIds = idPerfs.map((p) => p.stored_id); - setPerformerIds(newIds as string[]); + formik.setFieldValue("performer_ids", newIds as string[]); } } @@ -326,18 +363,18 @@ export const GalleryEditPanel: React.FC< if (idTags.length > 0) { const newIds = idTags.map((t) => t.stored_id); - setTagIds(newIds as string[]); + formik.setFieldValue("tag_ids", newIds as string[]); } } } async function onScrapeGalleryURL() { - if (!url) { + if (!formik.values.url) { return; } setIsLoading(true); try { - const result = await queryScrapeGalleryURL(url); + const result = await queryScrapeGalleryURL(formik.values.url); if (!result || !result.data || !result.data.scrapeGalleryURL) { return; } @@ -350,7 +387,7 @@ export const GalleryEditPanel: React.FC< } function maybeRenderScrapeButton() { - if (!url || !urlScrapable(url)) { + if (!formik.values.url || !urlScrapable(formik.values.url)) { return undefined; } return ( @@ -364,158 +401,197 @@ export const GalleryEditPanel: React.FC< ); } + function renderTextField(field: string, title: string, placeholder?: string) { + return ( + + {FormUtils.renderLabel({ + title, + })} + + + + {formik.getFieldMeta(field).error} + + + + ); + } + if (isLoading) return ; return (