Touch up Performer Page (#2200)

* Moves "Edit" and "Autotag" out of performer tabs
* Smoothen out fedit submission behavior
This commit is contained in:
kermieisinthehouse
2022-01-04 19:18:57 -08:00
committed by GitHub
parent be5dc7e545
commit baf148625c
12 changed files with 160 additions and 188 deletions

View File

@@ -1,4 +1,5 @@
### 🎨 Improvements ### 🎨 Improvements
* Made Performer page consistent with Studio and Tag pages. ([#2200](https://github.com/stashapp/stash/pull/2200))
* Add gender icons to performers. ([#2179](https://github.com/stashapp/stash/pull/2179)) * Add gender icons to performers. ([#2179](https://github.com/stashapp/stash/pull/2179))
* Add button to test credentials when adding/editing stash-box endpoints. ([#2173](https://github.com/stashapp/stash/pull/2173)) * Add button to test credentials when adding/editing stash-box endpoints. ([#2173](https://github.com/stashapp/stash/pull/2173))
* Show counts on list tabs in Performer, Studio and Tag pages. ([#2169](https://github.com/stashapp/stash/pull/2169)) * Show counts on list tabs in Performer, Studio and Tag pages. ([#2169](https://github.com/stashapp/stash/pull/2169))

View File

@@ -470,7 +470,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
</Form> </Form>
<DetailsEditNavbar <DetailsEditNavbar
objectName={movie?.name ?? "movie"} objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
isNew={isNew} isNew={isNew}
isEditing={isEditing} isEditing={isEditing}
onToggleEdit={onCancel} onToggleEdit={onCancel}

View File

@@ -24,7 +24,7 @@ const GenderIcon: React.FC<IIconProps> = ({ gender, className }) => {
: faTransgenderAlt; : faTransgenderAlt;
return ( return (
<FontAwesomeIcon <FontAwesomeIcon
title={intl.formatMessage({ id: "gender." + gender })} title={intl.formatMessage({ id: "gender_types." + gender })}
className={className} className={className}
icon={icon} icon={icon}
/> />

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { Button, Tabs, Tab, Badge } from "react-bootstrap"; import { Button, Tabs, Tab, Badge, Col, Row } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
@@ -10,9 +10,11 @@ import {
useFindPerformer, useFindPerformer,
usePerformerUpdate, usePerformerUpdate,
usePerformerDestroy, usePerformerDestroy,
mutateMetadataAutoTag,
} from "src/core/StashService"; } from "src/core/StashService";
import { import {
CountryFlag, CountryFlag,
DetailsEditNavbar,
ErrorMessage, ErrorMessage,
Icon, Icon,
LoadingIndicator, LoadingIndicator,
@@ -21,7 +23,6 @@ import { useLightbox, useToast } from "src/hooks";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
import { PerformerDetailsPanel } from "./PerformerDetailsPanel"; import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
import { PerformerScenesPanel } from "./PerformerScenesPanel"; import { PerformerScenesPanel } from "./PerformerScenesPanel";
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel"; import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
import { PerformerMoviesPanel } from "./PerformerMoviesPanel"; import { PerformerMoviesPanel } from "./PerformerMoviesPanel";
@@ -44,6 +45,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
const [imagePreview, setImagePreview] = useState<string | null>(); const [imagePreview, setImagePreview] = useState<string | null>();
const [imageEncoding, setImageEncoding] = useState<boolean>(false); const [imageEncoding, setImageEncoding] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(false);
// if undefined then get the existing image // if undefined then get the existing image
// if null then get the default (no) image // if null then get the default (no) image
@@ -68,9 +70,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
tab === "scenes" || tab === "scenes" ||
tab === "galleries" || tab === "galleries" ||
tab === "images" || tab === "images" ||
tab === "movies" || tab === "movies"
tab === "edit" ||
tab === "operations"
? tab ? tab
: "details"; : "details";
const setActiveTabKey = (newTab: string | null) => { const setActiveTabKey = (newTab: string | null) => {
@@ -84,14 +84,24 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding); const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
async function onAutoTag() {
try {
await mutateMetadataAutoTag({ performers: [performer.id] });
Toast.success({
content: intl.formatMessage({ id: "toast.started_auto_tagging" }),
});
} catch (e) {
Toast.error(e);
}
}
// set up hotkeys // set up hotkeys
useEffect(() => { useEffect(() => {
Mousetrap.bind("a", () => setActiveTabKey("details")); Mousetrap.bind("a", () => setActiveTabKey("details"));
Mousetrap.bind("e", () => setActiveTabKey("edit")); Mousetrap.bind("e", () => setIsEditing(!isEditing));
Mousetrap.bind("c", () => setActiveTabKey("scenes")); Mousetrap.bind("c", () => setActiveTabKey("scenes"));
Mousetrap.bind("g", () => setActiveTabKey("galleries")); Mousetrap.bind("g", () => setActiveTabKey("galleries"));
Mousetrap.bind("m", () => setActiveTabKey("movies")); Mousetrap.bind("m", () => setActiveTabKey("movies"));
Mousetrap.bind("o", () => setActiveTabKey("operations"));
Mousetrap.bind("f", () => setFavorite(!performer.favorite)); Mousetrap.bind("f", () => setFavorite(!performer.favorite));
// numeric keypresses get caught by jwplayer, so blur the element // numeric keypresses get caught by jwplayer, so blur the element
@@ -139,85 +149,108 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
} }
const renderTabs = () => ( const renderTabs = () => (
<Tabs <React.Fragment>
activeKey={activeTabKey} <Col>
onSelect={setActiveTabKey} <Row xs={8}>
id="performer-details" <DetailsEditNavbar
unmountOnExit objectName={
> performer?.name ?? intl.formatMessage({ id: "performer" })
<Tab eventKey="details" title={intl.formatMessage({ id: "details" })}> }
<PerformerDetailsPanel performer={performer} /> onToggleEdit={() => {
</Tab> setIsEditing(!isEditing);
<Tab }}
eventKey="scenes" onDelete={onDelete}
title={ onAutoTag={onAutoTag}
<React.Fragment> isNew={false}
{intl.formatMessage({ id: "scenes" })} isEditing={false}
<Badge className="left-spacing" pill variant="secondary"> onSave={() => {}}
{intl.formatNumber(performer.scene_count ?? 0)} onImageChange={() => {}}
</Badge> />
</React.Fragment> </Row>
} </Col>
<Tabs
activeKey={activeTabKey}
onSelect={setActiveTabKey}
id="performer-details"
unmountOnExit
> >
<PerformerScenesPanel performer={performer} /> <Tab eventKey="details" title={intl.formatMessage({ id: "details" })}>
</Tab> <PerformerDetailsPanel performer={performer} />
<Tab </Tab>
eventKey="galleries" <Tab
title={ eventKey="scenes"
<React.Fragment> title={
{intl.formatMessage({ id: "galleries" })} <React.Fragment>
<Badge className="left-spacing" pill variant="secondary"> {intl.formatMessage({ id: "scenes" })}
{intl.formatNumber(performer.gallery_count ?? 0)} <Badge className="left-spacing" pill variant="secondary">
</Badge> {intl.formatNumber(performer.scene_count ?? 0)}
</React.Fragment> </Badge>
} </React.Fragment>
> }
<PerformerGalleriesPanel performer={performer} /> >
</Tab> <PerformerScenesPanel performer={performer} />
<Tab </Tab>
eventKey="images" <Tab
title={ eventKey="galleries"
<React.Fragment> title={
{intl.formatMessage({ id: "images" })} <React.Fragment>
<Badge className="left-spacing" pill variant="secondary"> {intl.formatMessage({ id: "galleries" })}
{intl.formatNumber(performer.image_count ?? 0)} <Badge className="left-spacing" pill variant="secondary">
</Badge> {intl.formatNumber(performer.gallery_count ?? 0)}
</React.Fragment> </Badge>
} </React.Fragment>
> }
<PerformerImagesPanel performer={performer} /> >
</Tab> <PerformerGalleriesPanel performer={performer} />
<Tab </Tab>
eventKey="movies" <Tab
title={ eventKey="images"
<React.Fragment> title={
{intl.formatMessage({ id: "movies" })} <React.Fragment>
<Badge className="left-spacing" pill variant="secondary"> {intl.formatMessage({ id: "images" })}
{intl.formatNumber(performer.movie_count ?? 0)} <Badge className="left-spacing" pill variant="secondary">
</Badge> {intl.formatNumber(performer.image_count ?? 0)}
</React.Fragment> </Badge>
} </React.Fragment>
> }
<PerformerMoviesPanel performer={performer} /> >
</Tab> <PerformerImagesPanel performer={performer} />
<Tab eventKey="edit" title={intl.formatMessage({ id: "actions.edit" })}> </Tab>
<Tab
eventKey="movies"
title={
<React.Fragment>
{intl.formatMessage({ id: "movies" })}
<Badge className="left-spacing" pill variant="secondary">
{intl.formatNumber(performer.movie_count ?? 0)}
</Badge>
</React.Fragment>
}
>
<PerformerMoviesPanel performer={performer} />
</Tab>
</Tabs>
</React.Fragment>
);
function renderTabsOrEditPanel() {
if (isEditing) {
return (
<PerformerEditPanel <PerformerEditPanel
performer={performer} performer={performer}
isVisible={activeTabKey === "edit"} isVisible={isEditing}
isNew={false} isNew={false}
onDelete={onDelete}
onImageChange={onImageChange} onImageChange={onImageChange}
onImageEncoding={onImageEncoding} onImageEncoding={onImageEncoding}
onCancelEditing={() => {
setIsEditing(false);
}}
/> />
</Tab> );
<Tab } else {
eventKey="operations" return renderTabs();
title={intl.formatMessage({ id: "operations" })} }
> }
<PerformerOperationsPanel performer={performer} />
</Tab>
</Tabs>
);
function maybeRenderAge() { function maybeRenderAge() {
if (performer?.birthdate) { if (performer?.birthdate) {
@@ -375,7 +408,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
</div> </div>
</div> </div>
<div className="performer-body"> <div className="performer-body">
<div className="performer-tabs">{renderTabs()}</div> <div className="performer-tabs">{renderTabsOrEditPanel()}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -96,10 +96,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
return ( return (
<dl className="details-list"> <dl className="details-list">
<TextField <TextField
id="gender.gender" id="gender"
value={ value={
performer.gender performer.gender
? intl.formatMessage({ id: "gender." + performer.gender }) ? intl.formatMessage({ id: "gender_types." + performer.gender })
: undefined : undefined
} }
/> />

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Form, Col, Row, Badge, Dropdown } from "react-bootstrap"; import { Button, Form, Col, Row, Badge, Dropdown } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage } from "react-intl";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
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";
@@ -18,7 +18,6 @@ import {
ImageInput, ImageInput,
LoadingIndicator, LoadingIndicator,
CollapseButton, CollapseButton,
Modal,
TagSelect, TagSelect,
URLField, URLField,
} from "src/components/Shared"; } from "src/components/Shared";
@@ -46,20 +45,19 @@ interface IPerformerDetails {
performer: Partial<GQL.PerformerDataFragment>; performer: Partial<GQL.PerformerDataFragment>;
isNew?: boolean; isNew?: boolean;
isVisible: boolean; isVisible: boolean;
onDelete?: () => void;
onImageChange?: (image?: string | null) => void; onImageChange?: (image?: string | null) => void;
onImageEncoding?: (loading?: boolean) => void; onImageEncoding?: (loading?: boolean) => void;
onCancelEditing?: () => void;
} }
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
performer, performer,
isNew, isNew,
isVisible, isVisible,
onDelete,
onImageChange, onImageChange,
onImageEncoding, onImageEncoding,
onCancelEditing,
}) => { }) => {
const intl = useIntl();
const Toast = useToast(); const Toast = useToast();
const history = useHistory(); const history = useHistory();
@@ -67,7 +65,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const [scraper, setScraper] = useState<GQL.Scraper | IStashBox | undefined>(); const [scraper, setScraper] = useState<GQL.Scraper | IStashBox | undefined>();
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>(); const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>();
const [isScraperModalOpen, setIsScraperModalOpen] = useState<boolean>(false); const [isScraperModalOpen, setIsScraperModalOpen] = useState<boolean>(false);
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -361,7 +358,17 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
async function onSave(performerInput: InputValues) { async function onSave(performerInput: InputValues) {
setIsLoading(true); setIsLoading(true);
try { try {
if (!isNew) { if (isNew) {
const input = getCreateValues(performerInput);
const result = await createPerformer({
variables: {
input,
},
});
if (result.data?.performerCreate) {
history.push(`/performers/${result.data.performerCreate.id}`);
}
} else {
const input = getUpdateValues(performerInput); const input = getUpdateValues(performerInput);
await updatePerformer({ await updatePerformer({
@@ -372,20 +379,14 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
}, },
}, },
}); });
history.push(`/performers/${performer.id}`);
} else {
const input = getCreateValues(performerInput);
const result = await createPerformer({
variables: {
input,
},
});
if (result.data?.performerCreate) {
history.push(`/performers/${result.data.performerCreate.id}`);
}
} }
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);
setIsLoading(false);
return;
}
if (!isNew && onCancelEditing) {
onCancelEditing();
} }
setIsLoading(false); setIsLoading(false);
} }
@@ -397,12 +398,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
onSave?.(formik.values); onSave?.(formik.values);
}); });
if (!isNew) {
Mousetrap.bind("d d", () => {
setIsDeleteAlertOpen(true);
});
}
return () => { return () => {
Mousetrap.unbind("s s"); Mousetrap.unbind("s s");
@@ -655,25 +650,17 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
setScraper(undefined); setScraper(undefined);
} }
function renderButtons() { function renderButtons(classNames: string) {
return ( return (
<Row> <Row>
<Col className="mt-3" xs={12}> <Col className={classNames} xs={12}>
<Button {!isNew && onCancelEditing ? (
className="mr-2"
variant="primary"
disabled={!formik.dirty}
onClick={() => formik.submitForm()}
>
<FormattedMessage id="actions.save" />
</Button>
{!isNew ? (
<Button <Button
className="mr-2" className="mr-2"
variant="danger" variant="primary"
onClick={() => setIsDeleteAlertOpen(true)} onClick={() => onCancelEditing()}
> >
<FormattedMessage id="actions.delete" /> <FormattedMessage id="actions.cancel" />
</Button> </Button>
) : ( ) : (
"" ""
@@ -685,12 +672,19 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
onImageURL={onImageChangeURL} onImageURL={onImageChangeURL}
/> />
<Button <Button
className="mx-2" className="mr-2"
variant="danger" variant="danger"
onClick={() => formik.setFieldValue("image", null)} onClick={() => formik.setFieldValue("image", null)}
> >
<FormattedMessage id="actions.clear_image" /> <FormattedMessage id="actions.clear_image" />
</Button> </Button>
<Button
variant="success"
disabled={!formik.dirty}
onClick={() => formik.submitForm()}
>
<FormattedMessage id="actions.save" />
</Button>
</Col> </Col>
</Row> </Row>
); );
@@ -716,28 +710,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
) : undefined; ) : undefined;
}; };
function renderDeleteAlert() {
return (
<Modal
show={isDeleteAlertOpen}
icon="trash-alt"
accept={{
text: intl.formatMessage({ id: "actions.delete" }),
variant: "danger",
onClick: onDelete,
}}
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
>
<p>
<FormattedMessage
id="dialogs.delete_confirm"
values={{ entityName: performer.name }}
/>
</p>
</Modal>
);
}
function renderTagsField() { function renderTagsField() {
return ( return (
<Form.Group controlId="tags" as={Row}> <Form.Group controlId="tags" as={Row}>
@@ -837,7 +809,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
return ( return (
<> <>
{renderDeleteAlert()}
{renderScrapeModal()} {renderScrapeModal()}
{maybeRenderScrapeDialog()} {maybeRenderScrapeDialog()}
@@ -845,6 +816,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
when={formik.dirty} when={formik.dirty}
message="Unsaved changes. Are you sure you want to leave?" message="Unsaved changes. Are you sure you want to leave?"
/> />
{renderButtons("mb-3")}
<Form noValidate onSubmit={formik.handleSubmit} id="performer-edit"> <Form noValidate onSubmit={formik.handleSubmit} id="performer-edit">
<Form.Group controlId="name" as={Row}> <Form.Group controlId="name" as={Row}>
@@ -880,7 +852,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Form.Group as={Row}> <Form.Group as={Row}>
<Form.Label column xs={labelXS} xl={labelXL}> <Form.Label column xs={labelXS} xl={labelXL}>
<FormattedMessage id="gender.gender" /> <FormattedMessage id="gender" />
</Form.Label> </Form.Label>
<Col xs="auto"> <Col xs="auto">
<Form.Control <Form.Control
@@ -970,7 +942,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
{renderStashIDs()} {renderStashIDs()}
{renderButtons()} {renderButtons("mt-3")}
</Form> </Form>
</> </>
); );

View File

@@ -1,34 +0,0 @@
import { Button } from "react-bootstrap";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { mutateMetadataAutoTag } from "src/core/StashService";
import { useToast } from "src/hooks";
interface IPerformerOperationsProps {
performer: GQL.PerformerDataFragment;
}
export const PerformerOperationsPanel: React.FC<IPerformerOperationsProps> = ({
performer,
}) => {
const Toast = useToast();
const intl = useIntl();
async function onAutoTag() {
try {
await mutateMetadataAutoTag({ performers: [performer.id] });
Toast.success({
content: intl.formatMessage({ id: "toast.started_auto_tagging" }),
});
} catch (e) {
Toast.error(e);
}
}
return (
<Button onClick={onAutoTag}>
<FormattedMessage id="actions.auto_tag" />
</Button>
);
};

View File

@@ -429,7 +429,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
onChange={(value) => setAliases(value)} onChange={(value) => setAliases(value)}
/> />
{renderScrapedGenderRow( {renderScrapedGenderRow(
intl.formatMessage({ id: "gender.gender" }), intl.formatMessage({ id: "gender" }),
gender, gender,
(value) => setGender(value) (value) => setGender(value)
)} )}

View File

@@ -318,7 +318,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
</Form> </Form>
<DetailsEditNavbar <DetailsEditNavbar
objectName={studio?.name ?? "studio"} objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
isNew={isNew} isNew={isNew}
isEditing isEditing
onToggleEdit={onCancel} onToggleEdit={onCancel}

View File

@@ -192,7 +192,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
{renderField( {renderField(
"gender", "gender",
performer.gender performer.gender
? intl.formatMessage({ id: "gender." + performer.gender }) ? intl.formatMessage({ id: "gender_types." + performer.gender })
: "" : ""
)} )}
{renderField("birthdate", performer.birthdate)} {renderField("birthdate", performer.birthdate)}

View File

@@ -214,7 +214,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
</Form> </Form>
<DetailsEditNavbar <DetailsEditNavbar
objectName={tag?.name ?? "tag"} objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
isNew={isNew} isNew={isNew}
isEditing={isEditing} isEditing={isEditing}
onToggleEdit={onCancel} onToggleEdit={onCancel}

View File

@@ -691,8 +691,8 @@
"galleries": "Galleries", "galleries": "Galleries",
"gallery": "Gallery", "gallery": "Gallery",
"gallery_count": "Gallery Count", "gallery_count": "Gallery Count",
"gender": { "gender": "Gender",
"gender": "Gender", "gender_types": {
"MALE": "Male", "MALE": "Male",
"FEMALE": "Female", "FEMALE": "Female",
"TRANSGENDER_MALE": "Transgender Male", "TRANSGENDER_MALE": "Transgender Male",