mirror of https://github.com/stashapp/stash.git
Add scenes tab to performer page (#280)
* Make performer page tabbed * Add performer scenes tab * Make performer scenes criteria smarter * Adjust performer page layout. Add URL links * Add lightbox for performer image * Alias editing
This commit is contained in:
parent
7fdaccf669
commit
bab7c8f250
|
@ -1,11 +1,10 @@
|
|||
import {
|
||||
Button,
|
||||
Classes,
|
||||
Dialog,
|
||||
EditableText,
|
||||
HTMLTable,
|
||||
Spinner,
|
||||
FormGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button,
|
||||
AnchorButton,
|
||||
IconName,
|
||||
} from "@blueprintjs/core";
|
||||
import _ from "lodash";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
|
@ -13,77 +12,29 @@ import * as GQL from "../../../core/generated-graphql";
|
|||
import { StashService } from "../../../core/StashService";
|
||||
import { IBaseProps } from "../../../models";
|
||||
import { ErrorUtils } from "../../../utils/errors";
|
||||
import { TableUtils } from "../../../utils/table";
|
||||
import { ScrapePerformerSuggest } from "../../select/ScrapePerformerSuggest";
|
||||
import { DetailsEditNavbar } from "../../Shared/DetailsEditNavbar";
|
||||
import { ToastUtils } from "../../../utils/toasts";
|
||||
import { EditableTextUtils } from "../../../utils/editabletext";
|
||||
import { ImageUtils } from "../../../utils/image";
|
||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
|
||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||
import { TextUtils } from "../../../utils/text";
|
||||
import Lightbox from "react-images";
|
||||
|
||||
interface IPerformerProps extends IBaseProps {}
|
||||
|
||||
export const Performer: FunctionComponent<IPerformerProps> = (props: IPerformerProps) => {
|
||||
const isNew = props.match.params.id === "new";
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState<GQL.ListPerformerScrapersListPerformerScrapers | undefined>(undefined);
|
||||
const [scrapePerformerDetails, setScrapePerformerDetails] = useState<GQL.ScrapePerformerListScrapePerformerList | undefined>(undefined);
|
||||
|
||||
// Editing performer state
|
||||
const [image, setImage] = useState<string | undefined>(undefined);
|
||||
const [name, setName] = useState<string | undefined>(undefined);
|
||||
const [aliases, setAliases] = useState<string | undefined>(undefined);
|
||||
const [favorite, setFavorite] = useState<boolean | undefined>(undefined);
|
||||
const [birthdate, setBirthdate] = useState<string | undefined>(undefined);
|
||||
const [ethnicity, setEthnicity] = useState<string | undefined>(undefined);
|
||||
const [country, setCountry] = useState<string | undefined>(undefined);
|
||||
const [eyeColor, setEyeColor] = useState<string | undefined>(undefined);
|
||||
const [height, setHeight] = useState<string | undefined>(undefined);
|
||||
const [measurements, setMeasurements] = useState<string | undefined>(undefined);
|
||||
const [fakeTits, setFakeTits] = useState<string | undefined>(undefined);
|
||||
const [careerLength, setCareerLength] = useState<string | undefined>(undefined);
|
||||
const [tattoos, setTattoos] = useState<string | undefined>(undefined);
|
||||
const [piercings, setPiercings] = useState<string | undefined>(undefined);
|
||||
const [url, setUrl] = useState<string | undefined>(undefined);
|
||||
const [twitter, setTwitter] = useState<string | undefined>(undefined);
|
||||
const [instagram, setInstagram] = useState<string | undefined>(undefined);
|
||||
|
||||
// Performer state
|
||||
const [performer, setPerformer] = useState<Partial<GQL.PerformerDataFragment>>({});
|
||||
const [imagePreview, setImagePreview] = useState<string | undefined>(undefined);
|
||||
const [lightboxIsOpen, setLightboxIsOpen] = useState<boolean>(false);
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const Scrapers = StashService.useListPerformerScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.ListPerformerScrapersListPerformerScrapers[]>([]);
|
||||
|
||||
const { data, error, loading } = StashService.useFindPerformer(props.match.params.id);
|
||||
const updatePerformer = StashService.usePerformerUpdate(getPerformerInput() as GQL.PerformerUpdateInput);
|
||||
const createPerformer = StashService.usePerformerCreate(getPerformerInput() as GQL.PerformerCreateInput);
|
||||
const deletePerformer = StashService.usePerformerDestroy(getPerformerInput() as GQL.PerformerDestroyInput);
|
||||
|
||||
function updatePerformerEditState(state: Partial<GQL.PerformerDataFragment | GQL.ScrapeFreeonesScrapeFreeones>) {
|
||||
if ((state as GQL.PerformerDataFragment).favorite !== undefined) {
|
||||
setFavorite((state as GQL.PerformerDataFragment).favorite);
|
||||
}
|
||||
setName(state.name);
|
||||
setAliases(state.aliases);
|
||||
setBirthdate(state.birthdate);
|
||||
setEthnicity(state.ethnicity);
|
||||
setCountry(state.country);
|
||||
setEyeColor(state.eye_color);
|
||||
setHeight(state.height);
|
||||
setMeasurements(state.measurements);
|
||||
setFakeTits(state.fake_tits);
|
||||
setCareerLength(state.career_length);
|
||||
setTattoos(state.tattoos);
|
||||
setPiercings(state.piercings);
|
||||
setUrl(state.url);
|
||||
setTwitter(state.twitter);
|
||||
setInstagram(state.instagram);
|
||||
}
|
||||
const updatePerformer = StashService.usePerformerUpdate();
|
||||
const createPerformer = StashService.usePerformerCreate();
|
||||
const deletePerformer = StashService.usePerformerDestroy();
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(loading);
|
||||
|
@ -93,73 +44,25 @@ export const Performer: FunctionComponent<IPerformerProps> = (props: IPerformerP
|
|||
|
||||
useEffect(() => {
|
||||
setImagePreview(performer.image_path);
|
||||
setImage(undefined);
|
||||
updatePerformerEditState(performer);
|
||||
if (!isNew) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
}, [performer]);
|
||||
|
||||
function onImageLoad(this: FileReader) {
|
||||
setImagePreview(this.result as string);
|
||||
setImage(this.result as string);
|
||||
function onImageChange(image: string) {
|
||||
setImagePreview(image);
|
||||
}
|
||||
|
||||
ImageUtils.addPasteImageHook(onImageLoad);
|
||||
|
||||
useEffect(() => {
|
||||
var newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = [];
|
||||
|
||||
if (!!Scrapers.data && Scrapers.data.listPerformerScrapers) {
|
||||
newQueryableScrapers = Scrapers.data.listPerformerScrapers.filter((s) => {
|
||||
return s.performer && s.performer.supported_scrapes.includes(GQL.ScrapeType.Name);
|
||||
});
|
||||
}
|
||||
|
||||
setQueryableScrapers(newQueryableScrapers);
|
||||
|
||||
}, [Scrapers.data]);
|
||||
|
||||
if ((!isNew && !isEditing && (!data || !data.findPerformer)) || isLoading) {
|
||||
if ((!isNew && (!data || !data.findPerformer)) || isLoading) {
|
||||
return <Spinner size={Spinner.SIZE_LARGE} />;
|
||||
}
|
||||
if (!!error) { return <>error...</>; }
|
||||
|
||||
function getPerformerInput() {
|
||||
const performerInput: Partial<GQL.PerformerCreateInput | GQL.PerformerUpdateInput> = {
|
||||
name,
|
||||
aliases,
|
||||
favorite,
|
||||
birthdate,
|
||||
ethnicity,
|
||||
country,
|
||||
eye_color: eyeColor,
|
||||
height,
|
||||
measurements,
|
||||
fake_tits: fakeTits,
|
||||
career_length: careerLength,
|
||||
tattoos,
|
||||
piercings,
|
||||
url,
|
||||
twitter,
|
||||
instagram,
|
||||
image,
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
(performerInput as GQL.PerformerUpdateInput).id = props.match.params.id;
|
||||
}
|
||||
return performerInput;
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
async function onSave(performer : Partial<GQL.PerformerCreateInput> | Partial<GQL.PerformerUpdateInput>) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!isNew) {
|
||||
const result = await updatePerformer();
|
||||
const result = await updatePerformer({variables: performer as GQL.PerformerUpdateInput});
|
||||
setPerformer(result.data.performerUpdate);
|
||||
} else {
|
||||
const result = await createPerformer();
|
||||
const result = await createPerformer({variables: performer as GQL.PerformerCreateInput});
|
||||
setPerformer(result.data.performerCreate);
|
||||
props.history.push(`/performers/${result.data.performerCreate.id}`);
|
||||
}
|
||||
|
@ -172,7 +75,7 @@ export const Performer: FunctionComponent<IPerformerProps> = (props: IPerformerP
|
|||
async function onDelete() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await deletePerformer();
|
||||
await deletePerformer({variables: {id: props.match.params.id}});
|
||||
} catch (e) {
|
||||
ErrorUtils.handle(e);
|
||||
}
|
||||
|
@ -182,214 +85,164 @@ export const Performer: FunctionComponent<IPerformerProps> = (props: IPerformerP
|
|||
props.history.push(`/performers`);
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
if (!performer || !performer.id) {
|
||||
return;
|
||||
function renderTabs() {
|
||||
function renderEditPanel() {
|
||||
return (
|
||||
<PerformerDetailsPanel
|
||||
performer={performer}
|
||||
isEditing={true}
|
||||
isNew={isNew}
|
||||
onDelete={onDelete}
|
||||
onSave={onSave}
|
||||
onImageChange={onImageChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
try {
|
||||
await StashService.queryMetadataAutoTag({ performers: [performer.id]});
|
||||
ToastUtils.success("Started auto tagging");
|
||||
} catch (e) {
|
||||
ErrorUtils.handle(e);
|
||||
|
||||
// render tabs if not new
|
||||
if (!isNew) {
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
renderActiveTabPanelOnly={true}
|
||||
large={true}
|
||||
>
|
||||
<Tab id="performer-details-panel" title="Details" panel={<PerformerDetailsPanel performer={performer} isEditing={false}/>} />
|
||||
<Tab id="performer-scenes-panel" title="Scenes" panel={<PerformerScenesPanel performer={performer} base={props} />} />
|
||||
<Tab id="performer-edit-panel" title="Edit" panel={renderEditPanel()} />
|
||||
<Tab id="performer-operations-panel" title="Operations" panel={<PerformerOperationsPanel performer={performer} />} />
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return renderEditPanel();
|
||||
}
|
||||
}
|
||||
|
||||
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
function onDisplayFreeOnesDialog(scraper: GQL.ListPerformerScrapersListPerformerScrapers) {
|
||||
setIsDisplayingScraperDialog(scraper);
|
||||
}
|
||||
|
||||
function getQueryScraperPerformerInput() {
|
||||
if (!scrapePerformerDetails) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let ret = _.clone(scrapePerformerDetails);
|
||||
delete ret.__typename;
|
||||
return ret as GQL.ScrapedPerformerInput;
|
||||
}
|
||||
|
||||
async function onScrapePerformer() {
|
||||
setIsDisplayingScraperDialog(undefined);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!scrapePerformerDetails || !isDisplayingScraperDialog) { return; }
|
||||
const result = await StashService.queryScrapePerformer(isDisplayingScraperDialog.id, getQueryScraperPerformerInput());
|
||||
if (!result.data || !result.data.scrapePerformer) { return; }
|
||||
updatePerformerEditState(result.data.scrapePerformer);
|
||||
} catch (e) {
|
||||
ErrorUtils.handle(e);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
async function onScrapePerformerURL() {
|
||||
if (!url) { return; }
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await StashService.queryScrapePerformerURL(url);
|
||||
if (!result.data || !result.data.scrapePerformerURL) { return; }
|
||||
|
||||
// leave URL as is if not set explicitly
|
||||
if (!result.data.scrapePerformerURL.url) {
|
||||
result.data.scrapePerformerURL.url = url;
|
||||
}
|
||||
updatePerformerEditState(result.data.scrapePerformerURL);
|
||||
} catch (e) {
|
||||
ErrorUtils.handle(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEthnicity() {
|
||||
return TableUtils.renderHtmlSelect({
|
||||
title: "Ethnicity",
|
||||
value: ethnicity,
|
||||
isEditing,
|
||||
onChange: (value: string) => setEthnicity(value),
|
||||
selectOptions: ["white", "black", "asian", "hispanic"],
|
||||
});
|
||||
}
|
||||
|
||||
function renderScraperDialog() {
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={!!isDisplayingScraperDialog}
|
||||
onClose={() => setIsDisplayingScraperDialog(undefined)}
|
||||
title="Scrape"
|
||||
>
|
||||
<div className="dialog-content">
|
||||
<ScrapePerformerSuggest
|
||||
placeholder="Performer name"
|
||||
style={{width: "100%"}}
|
||||
scraperId={isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""}
|
||||
onSelectPerformer={(query) => setScrapePerformerDetails(query)}
|
||||
/>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={() => onScrapePerformer()}>Scrape</Button>
|
||||
function maybeRenderAge() {
|
||||
if (performer && performer.birthdate) {
|
||||
// calculate the age from birthdate. In future, this should probably be
|
||||
// provided by the server
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span className="age">{TextUtils.age(performer.birthdate)}</span>
|
||||
<span className="age-tail"> years old</span>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function urlScrapable(url: string) : boolean {
|
||||
return !!url && !!Scrapers.data && Scrapers.data.listPerformerScrapers && Scrapers.data.listPerformerScrapers.some((s) => {
|
||||
return !!s.performer && !!s.performer.urls && s.performer.urls.some((u) => { return url.includes(u); });
|
||||
});
|
||||
}
|
||||
|
||||
function maybeRenderScrapeButton() {
|
||||
if (!url || !isEditing || !urlScrapable(url)) {
|
||||
return undefined;
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
minimal={true}
|
||||
icon="import"
|
||||
id="scrape-url-button"
|
||||
onClick={() => onScrapePerformerURL()}/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderURLField() {
|
||||
function maybeRenderAliases() {
|
||||
if (performer && performer.aliases) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span className="alias-head">Also known as </span>
|
||||
<span className="alias">{performer.aliases}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setFavorite(v : boolean) {
|
||||
performer.favorite = v;
|
||||
onSave(performer);
|
||||
}
|
||||
|
||||
function renderIcons() {
|
||||
function maybeRenderURL(url?: string, icon?: IconName) {
|
||||
if (performer.url) {
|
||||
if (!icon) {
|
||||
icon = "link";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnchorButton
|
||||
icon={icon}
|
||||
href={performer.url}
|
||||
minimal={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td id="url-field">
|
||||
URL
|
||||
{maybeRenderScrapeButton()}
|
||||
</td>
|
||||
<td>
|
||||
{EditableTextUtils.renderInputGroup({
|
||||
value: url, isEditing, onChange: setUrl, placeholder: "URL"
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
<>
|
||||
<span className="name-icons">
|
||||
<Button
|
||||
icon="heart"
|
||||
className={performer.favorite ? "favorite" : "not-favorite"}
|
||||
onClick={() => setFavorite(!performer.favorite)}
|
||||
minimal={true}
|
||||
/>
|
||||
{maybeRenderURL(performer.url)}
|
||||
{/* TODO - render instagram and twitter links with icons */}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderScraperDialog()}
|
||||
function renderNewView() {
|
||||
return (
|
||||
<div className="columns is-multiline no-spacing">
|
||||
<div className="column is-half details-image-container">
|
||||
<img className="performer" src={imagePreview} />
|
||||
</div>
|
||||
<div className="column is-half details-detail-container">
|
||||
<DetailsEditNavbar
|
||||
performer={performer}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => { setIsEditing(!isEditing); updatePerformerEditState(performer); }}
|
||||
onSave={onSave}
|
||||
onDelete={onDelete}
|
||||
onImageChange={onImageChange}
|
||||
scrapers={queryableScrapers}
|
||||
onDisplayScraperDialog={onDisplayFreeOnesDialog}
|
||||
onAutoTag={onAutoTag}
|
||||
/>
|
||||
<h1 className="bp3-heading">
|
||||
<EditableText
|
||||
disabled={!isEditing}
|
||||
value={name}
|
||||
placeholder="Name"
|
||||
onChange={(value) => setName(value)}
|
||||
/>
|
||||
</h1>
|
||||
<h6 className="bp3-heading">
|
||||
<FormGroup className="aliases-field" inline={true} label="Aliases:">
|
||||
{EditableTextUtils.renderInputGroup({
|
||||
value: aliases, isEditing: isEditing, placeholder: "Aliases", onChange: setAliases
|
||||
})}
|
||||
</FormGroup>
|
||||
</h6>
|
||||
<div>
|
||||
<span style={{fontWeight: 300}}>Favorite:</span>
|
||||
<Button
|
||||
icon="heart"
|
||||
disabled={!isEditing}
|
||||
className={favorite ? "favorite" : undefined}
|
||||
onClick={() => setFavorite(!favorite)}
|
||||
minimal={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HTMLTable id="performer-details" style={{width: "100%"}}>
|
||||
<tbody>
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Birthdate (YYYY-MM-DD)", value: birthdate, isEditing, onChange: setBirthdate})}
|
||||
{renderEthnicity()}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Eye Color", value: eyeColor, isEditing, onChange: setEyeColor})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Country", value: country, isEditing, onChange: setCountry})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Height (CM)", value: height, isEditing, onChange: setHeight})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Measurements", value: measurements, isEditing, onChange: setMeasurements})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Fake Tits", value: fakeTits, isEditing, onChange: setFakeTits})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Career Length", value: careerLength, isEditing, onChange: setCareerLength})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Tattoos", value: tattoos, isEditing, onChange: setTattoos})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Piercings", value: piercings, isEditing, onChange: setPiercings})}
|
||||
{renderURLField()}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Twitter", value: twitter, isEditing, onChange: setTwitter})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Instagram", value: instagram, isEditing, onChange: setInstagram})}
|
||||
</tbody>
|
||||
</HTMLTable>
|
||||
{renderTabs()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const photos = [{src: imagePreview || "", caption: "Image"}];
|
||||
|
||||
function openLightbox() {
|
||||
setLightboxIsOpen(true);
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
setLightboxIsOpen(false);
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
return renderNewView();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="performer-page">
|
||||
<div className="details-image-container">
|
||||
<img className="performer" src={imagePreview} onClick={openLightbox} />
|
||||
</div>
|
||||
<div className="performer-head">
|
||||
<h1 className="bp3-heading">
|
||||
{performer.name}
|
||||
{renderIcons()}
|
||||
</h1>
|
||||
{maybeRenderAliases()}
|
||||
{maybeRenderAge()}
|
||||
</div>
|
||||
|
||||
<div className="performer-body">
|
||||
<div className="details-detail-container">
|
||||
{renderTabs()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Lightbox
|
||||
images={photos}
|
||||
onClose={closeLightbox}
|
||||
currentImage={0}
|
||||
isOpen={lightboxIsOpen}
|
||||
onClickImage={() => window.open(imagePreview, "_blank")}
|
||||
width={9999}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,404 @@
|
|||
import {
|
||||
Button,
|
||||
Classes,
|
||||
Dialog,
|
||||
EditableText,
|
||||
HTMLTable,
|
||||
Spinner,
|
||||
FormGroup,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Alert,
|
||||
FileInput,
|
||||
} from "@blueprintjs/core";
|
||||
import _ from "lodash";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as GQL from "../../../core/generated-graphql";
|
||||
import { StashService } from "../../../core/StashService";
|
||||
import { ErrorUtils } from "../../../utils/errors";
|
||||
import { TableUtils } from "../../../utils/table";
|
||||
import { ScrapePerformerSuggest } from "../../select/ScrapePerformerSuggest";
|
||||
import { ToastUtils } from "../../../utils/toasts";
|
||||
import { EditableTextUtils } from "../../../utils/editabletext";
|
||||
import { ImageUtils } from "../../../utils/image";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
performer: Partial<GQL.PerformerDataFragment>
|
||||
isNew?: boolean
|
||||
isEditing?: boolean
|
||||
onSave? : (performer : Partial<GQL.PerformerCreateInput> | Partial<GQL.PerformerUpdateInput>) => void
|
||||
onDelete? : () => void
|
||||
onImageChange? : (image: string) => void
|
||||
}
|
||||
|
||||
export const PerformerDetailsPanel: FunctionComponent<IPerformerDetailsProps> = (props: IPerformerDetailsProps) => {
|
||||
|
||||
// Editing state
|
||||
const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState<GQL.ListPerformerScrapersListPerformerScrapers | undefined>(undefined);
|
||||
const [scrapePerformerDetails, setScrapePerformerDetails] = useState<GQL.ScrapePerformerListScrapePerformerList | undefined>(undefined);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Editing performer state
|
||||
const [image, setImage] = useState<string | undefined>(undefined);
|
||||
const [name, setName] = useState<string | undefined>(undefined);
|
||||
const [aliases, setAliases] = useState<string | undefined>(undefined);
|
||||
const [favorite, setFavorite] = useState<boolean | undefined>(undefined);
|
||||
const [birthdate, setBirthdate] = useState<string | undefined>(undefined);
|
||||
const [ethnicity, setEthnicity] = useState<string | undefined>(undefined);
|
||||
const [country, setCountry] = useState<string | undefined>(undefined);
|
||||
const [eyeColor, setEyeColor] = useState<string | undefined>(undefined);
|
||||
const [height, setHeight] = useState<string | undefined>(undefined);
|
||||
const [measurements, setMeasurements] = useState<string | undefined>(undefined);
|
||||
const [fakeTits, setFakeTits] = useState<string | undefined>(undefined);
|
||||
const [careerLength, setCareerLength] = useState<string | undefined>(undefined);
|
||||
const [tattoos, setTattoos] = useState<string | undefined>(undefined);
|
||||
const [piercings, setPiercings] = useState<string | undefined>(undefined);
|
||||
const [url, setUrl] = useState<string | undefined>(undefined);
|
||||
const [twitter, setTwitter] = useState<string | undefined>(undefined);
|
||||
const [instagram, setInstagram] = useState<string | undefined>(undefined);
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const Scrapers = StashService.useListPerformerScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.ListPerformerScrapersListPerformerScrapers[]>([]);
|
||||
|
||||
function updatePerformerEditState(state: Partial<GQL.PerformerDataFragment | GQL.ScrapeFreeonesScrapeFreeones>) {
|
||||
if ((state as GQL.PerformerDataFragment).favorite !== undefined) {
|
||||
setFavorite((state as GQL.PerformerDataFragment).favorite);
|
||||
}
|
||||
setName(state.name);
|
||||
setAliases(state.aliases);
|
||||
setBirthdate(state.birthdate);
|
||||
setEthnicity(state.ethnicity);
|
||||
setCountry(state.country);
|
||||
setEyeColor(state.eye_color);
|
||||
setHeight(state.height);
|
||||
setMeasurements(state.measurements);
|
||||
setFakeTits(state.fake_tits);
|
||||
setCareerLength(state.career_length);
|
||||
setTattoos(state.tattoos);
|
||||
setPiercings(state.piercings);
|
||||
setUrl(state.url);
|
||||
setTwitter(state.twitter);
|
||||
setInstagram(state.instagram);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setImage(undefined);
|
||||
updatePerformerEditState(props.performer);
|
||||
}, [props.performer]);
|
||||
|
||||
function onImageLoad(this: FileReader) {
|
||||
setImage(this.result as string);
|
||||
if (props.onImageChange) {
|
||||
props.onImageChange(this.result as string);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.isEditing) {
|
||||
ImageUtils.addPasteImageHook(onImageLoad);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
var newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = [];
|
||||
|
||||
if (!!Scrapers.data && Scrapers.data.listPerformerScrapers) {
|
||||
newQueryableScrapers = Scrapers.data.listPerformerScrapers.filter((s) => {
|
||||
return s.performer && s.performer.supported_scrapes.includes(GQL.ScrapeType.Name);
|
||||
});
|
||||
}
|
||||
|
||||
setQueryableScrapers(newQueryableScrapers);
|
||||
|
||||
}, [Scrapers.data]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size={Spinner.SIZE_LARGE} />;
|
||||
}
|
||||
|
||||
function getPerformerInput() {
|
||||
const performerInput: Partial<GQL.PerformerCreateInput | GQL.PerformerUpdateInput> = {
|
||||
name,
|
||||
aliases,
|
||||
favorite,
|
||||
birthdate,
|
||||
ethnicity,
|
||||
country,
|
||||
eye_color: eyeColor,
|
||||
height,
|
||||
measurements,
|
||||
fake_tits: fakeTits,
|
||||
career_length: careerLength,
|
||||
tattoos,
|
||||
piercings,
|
||||
url,
|
||||
twitter,
|
||||
instagram,
|
||||
image,
|
||||
};
|
||||
|
||||
if (!props.isNew) {
|
||||
(performerInput as GQL.PerformerUpdateInput).id = props.performer.id!;
|
||||
}
|
||||
return performerInput;
|
||||
}
|
||||
|
||||
function onSave() {
|
||||
if (props.onSave) {
|
||||
props.onSave(getPerformerInput());
|
||||
}
|
||||
}
|
||||
|
||||
function onDelete() {
|
||||
if (props.onDelete) {
|
||||
props.onDelete();
|
||||
}
|
||||
}
|
||||
|
||||
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
function onDisplayFreeOnesDialog(scraper: GQL.ListPerformerScrapersListPerformerScrapers) {
|
||||
setIsDisplayingScraperDialog(scraper);
|
||||
}
|
||||
|
||||
function getQueryScraperPerformerInput() {
|
||||
if (!scrapePerformerDetails) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let ret = _.clone(scrapePerformerDetails);
|
||||
delete ret.__typename;
|
||||
return ret as GQL.ScrapedPerformerInput;
|
||||
}
|
||||
|
||||
async function onScrapePerformer() {
|
||||
setIsDisplayingScraperDialog(undefined);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!scrapePerformerDetails || !isDisplayingScraperDialog) { return; }
|
||||
const result = await StashService.queryScrapePerformer(isDisplayingScraperDialog.id, getQueryScraperPerformerInput());
|
||||
if (!result.data || !result.data.scrapePerformer) { return; }
|
||||
updatePerformerEditState(result.data.scrapePerformer);
|
||||
} catch (e) {
|
||||
ErrorUtils.handle(e);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
async function onScrapePerformerURL() {
|
||||
if (!url) { return; }
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await StashService.queryScrapePerformerURL(url);
|
||||
if (!result.data || !result.data.scrapePerformerURL) { return; }
|
||||
|
||||
// leave URL as is if not set explicitly
|
||||
if (!result.data.scrapePerformerURL.url) {
|
||||
result.data.scrapePerformerURL.url = url;
|
||||
}
|
||||
|
||||
updatePerformerEditState(result.data.scrapePerformerURL);
|
||||
} catch (e) {
|
||||
ErrorUtils.handle(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEthnicity() {
|
||||
return TableUtils.renderHtmlSelect({
|
||||
title: "Ethnicity",
|
||||
value: ethnicity,
|
||||
isEditing: !!props.isEditing,
|
||||
onChange: (value: string) => setEthnicity(value),
|
||||
selectOptions: ["white", "black", "asian", "hispanic"],
|
||||
});
|
||||
}
|
||||
|
||||
function renderScraperMenu() {
|
||||
function renderScraperMenuItem(scraper : GQL.ListPerformerScrapersListPerformerScrapers) {
|
||||
return (
|
||||
<MenuItem
|
||||
text={scraper.name}
|
||||
onClick={() => { onDisplayFreeOnesDialog(scraper); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!props.performer) { return; }
|
||||
if (!props.isEditing) { return; }
|
||||
const scraperMenu = (
|
||||
<Menu>
|
||||
{queryableScrapers ? queryableScrapers.map((s) => renderScraperMenuItem(s)) : undefined}
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<Popover content={scraperMenu} position="bottom">
|
||||
<Button text="Scrape with..."/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScraperDialog() {
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={!!isDisplayingScraperDialog}
|
||||
onClose={() => setIsDisplayingScraperDialog(undefined)}
|
||||
title="Scrape"
|
||||
>
|
||||
<div className="dialog-content">
|
||||
<ScrapePerformerSuggest
|
||||
placeholder="Performer name"
|
||||
style={{width: "100%"}}
|
||||
scraperId={isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""}
|
||||
onSelectPerformer={(query) => setScrapePerformerDetails(query)}
|
||||
/>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={() => onScrapePerformer()}>Scrape</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function urlScrapable(url: string) : boolean {
|
||||
return !!url && !!Scrapers.data && Scrapers.data.listPerformerScrapers && Scrapers.data.listPerformerScrapers.some((s) => {
|
||||
return !!s.performer && !!s.performer.urls && s.performer.urls.some((u) => { return url.includes(u); });
|
||||
});
|
||||
}
|
||||
|
||||
function maybeRenderScrapeButton() {
|
||||
if (!url || !props.isEditing || !urlScrapable(url)) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
minimal={true}
|
||||
icon="import"
|
||||
id="scrape-url-button"
|
||||
onClick={() => onScrapePerformerURL()}/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderURLField() {
|
||||
return (
|
||||
<tr>
|
||||
<td id="url-field">
|
||||
URL
|
||||
{maybeRenderScrapeButton()}
|
||||
</td>
|
||||
<td>
|
||||
{EditableTextUtils.renderInputGroup({
|
||||
value: url, asURL: true, isEditing: !!props.isEditing, onChange: setUrl, placeholder: "URL"
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function renderImageInput() {
|
||||
if (!props.isEditing) { return; }
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td><FileInput text="Choose image..." onInputChange={onImageChange} inputProps={{accept: ".jpg,.jpeg"}} /></td>
|
||||
</tr>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function maybeRenderButtons() {
|
||||
if (props.isEditing) {
|
||||
return (
|
||||
<>
|
||||
<Button className="edit-button" text="Save" intent="primary" onClick={() => onSave()}/>
|
||||
{!props.isNew ? <Button className="edit-button" text="Delete" intent="danger" onClick={() => setIsDeleteAlertOpen(true)}/> : undefined}
|
||||
{renderScraperMenu()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDeleteAlert() {
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
icon="trash"
|
||||
intent="danger"
|
||||
isOpen={isDeleteAlertOpen}
|
||||
onCancel={() => setIsDeleteAlertOpen(false)}
|
||||
onConfirm={() => onDelete()}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to delete {name}?
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderName() {
|
||||
if (props.isEditing) {
|
||||
return TableUtils.renderInputGroup(
|
||||
{title: "Name", value: name, isEditing: !!props.isEditing, placeholder: "Name", onChange: setName});
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderAliases() {
|
||||
if (props.isEditing) {
|
||||
return TableUtils.renderInputGroup(
|
||||
{title: "Aliases", value: aliases, isEditing: !!props.isEditing, placeholder: "Aliases", onChange: setAliases});
|
||||
}
|
||||
}
|
||||
|
||||
const twitterPrefix = "https://twitter.com/";
|
||||
const instagramPrefix = "https://www.instagram.com/";
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderDeleteAlert()}
|
||||
{renderScraperDialog()}
|
||||
|
||||
<HTMLTable id="performer-details" style={{width: "100%"}}>
|
||||
<tbody>
|
||||
{maybeRenderName()}
|
||||
{maybeRenderAliases()}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Birthdate (YYYY-MM-DD)", value: birthdate, isEditing: !!props.isEditing, onChange: setBirthdate})}
|
||||
{renderEthnicity()}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Eye Color", value: eyeColor, isEditing: !!props.isEditing, onChange: setEyeColor})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Country", value: country, isEditing: !!props.isEditing, onChange: setCountry})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Height (CM)", value: height, isEditing: !!props.isEditing, onChange: setHeight})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Measurements", value: measurements, isEditing: !!props.isEditing, onChange: setMeasurements})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Fake Tits", value: fakeTits, isEditing: !!props.isEditing, onChange: setFakeTits})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Career Length", value: careerLength, isEditing: !!props.isEditing, onChange: setCareerLength})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Tattoos", value: tattoos, isEditing: !!props.isEditing, onChange: setTattoos})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Piercings", value: piercings, isEditing: !!props.isEditing, onChange: setPiercings})}
|
||||
{renderURLField()}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Twitter", value: twitter, asURL: true, urlPrefix: twitterPrefix, isEditing: !!props.isEditing, onChange: setTwitter})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Instagram", value: instagram, asURL: true, urlPrefix: instagramPrefix, isEditing: !!props.isEditing, onChange: setInstagram})}
|
||||
{renderImageInput()}
|
||||
</tbody>
|
||||
</HTMLTable>
|
||||
|
||||
{maybeRenderButtons()}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
Button,
|
||||
} from "@blueprintjs/core";
|
||||
import _ from "lodash";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import * as GQL from "../../../core/generated-graphql";
|
||||
import { StashService } from "../../../core/StashService";
|
||||
import { ErrorUtils } from "../../../utils/errors";
|
||||
import { ToastUtils } from "../../../utils/toasts";
|
||||
|
||||
interface IPerformerOperationsProps {
|
||||
performer: Partial<GQL.PerformerDataFragment>
|
||||
}
|
||||
|
||||
export const PerformerOperationsPanel: FunctionComponent<IPerformerOperationsProps> = (props: IPerformerOperationsProps) => {
|
||||
|
||||
async function onAutoTag() {
|
||||
if (!props.performer || !props.performer.id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await StashService.queryMetadataAutoTag({ performers: [props.performer.id]});
|
||||
ToastUtils.success("Started auto tagging");
|
||||
} catch (e) {
|
||||
ErrorUtils.handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button text="Auto Tag" onClick={onAutoTag} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import _ from "lodash";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import * as GQL from "../../../core/generated-graphql";
|
||||
import { IBaseProps } from "../../../models";
|
||||
import { SceneList } from "../../scenes/SceneList";
|
||||
import { PerformersCriterion } from "../../../models/list-filter/criteria/performers";
|
||||
import { ListFilterModel } from "../../../models/list-filter/filter";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
performer: Partial<GQL.PerformerDataFragment>
|
||||
base: IBaseProps
|
||||
}
|
||||
|
||||
export const PerformerScenesPanel: FunctionComponent<IPerformerDetailsProps> = (props: IPerformerDetailsProps) => {
|
||||
|
||||
function filterHook(filter: ListFilterModel) {
|
||||
let performerValue = {id: props.performer.id!, label: props.performer.name!};
|
||||
// if performers is already present, then we modify it, otherwise add
|
||||
let performerCriterion = filter.criteria.find((c) => {
|
||||
return c.type === "performers";
|
||||
});
|
||||
|
||||
if (performerCriterion &&
|
||||
(performerCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
||||
performerCriterion.modifier === GQL.CriterionModifier.Includes)) {
|
||||
// add the performer if not present
|
||||
if (!performerCriterion.value.find((p : any) => {
|
||||
return p.id === props.performer.id;
|
||||
})) {
|
||||
performerCriterion.value.push(performerValue);
|
||||
}
|
||||
|
||||
performerCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||
} else {
|
||||
// overwrite
|
||||
performerCriterion = new PerformersCriterion();
|
||||
performerCriterion.value = [performerValue];
|
||||
filter.criteria.push(performerCriterion);
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
return (
|
||||
<SceneList
|
||||
base={props.base}
|
||||
subComponent={true}
|
||||
filterHook={filterHook}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -12,7 +12,11 @@ import { SceneListTable } from "./SceneListTable";
|
|||
import { SceneSelectedOptions } from "./SceneSelectedOptions";
|
||||
import { StashService } from "../../core/StashService";
|
||||
|
||||
interface ISceneListProps extends IBaseProps {}
|
||||
interface ISceneListProps {
|
||||
base : IBaseProps
|
||||
subComponent?: boolean
|
||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||
}
|
||||
|
||||
export const SceneList: FunctionComponent<ISceneListProps> = (props: ISceneListProps) => {
|
||||
const otherOperations = [
|
||||
|
@ -24,7 +28,9 @@ export const SceneList: FunctionComponent<ISceneListProps> = (props: ISceneListP
|
|||
|
||||
const listData = ListHook.useList({
|
||||
filterMode: FilterMode.Scenes,
|
||||
props,
|
||||
props: props.base,
|
||||
subComponent: props.subComponent,
|
||||
filterHook: props.filterHook,
|
||||
zoomable: true,
|
||||
otherOperations: otherOperations,
|
||||
renderContent,
|
||||
|
@ -44,7 +50,7 @@ export const SceneList: FunctionComponent<ISceneListProps> = (props: ISceneListP
|
|||
if (singleResult && singleResult.data && singleResult.data.findScenes && singleResult.data.findScenes.scenes.length === 1) {
|
||||
let id = singleResult!.data!.findScenes!.scenes[0].id;
|
||||
// navigate to the scene player page
|
||||
props.history.push("/scenes/" + id + "?autoplay=true");
|
||||
props.base.history.push("/scenes/" + id + "?autoplay=true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import _ from "lodash";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { IBaseProps } from "../../models/base-props";
|
||||
import { SceneList } from "./SceneList";
|
||||
|
||||
interface ISceneListPageProps extends IBaseProps {}
|
||||
|
||||
export const SceneListPage: FunctionComponent<ISceneListPageProps> = (props: ISceneListPageProps) => {
|
||||
return <SceneList base={props}/>;
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { Scene } from "./SceneDetails/Scene";
|
||||
import { SceneList } from "./SceneList";
|
||||
import { SceneMarkerList } from "./SceneMarkerList";
|
||||
import { SceneListPage } from "./SceneListPage";
|
||||
|
||||
const Scenes = () => (
|
||||
<Switch>
|
||||
<Route exact={true} path="/scenes" component={SceneList} />
|
||||
<Route exact={true} path="/scenes" component={SceneListPage} />
|
||||
<Route exact={true} path="/scenes/markers" component={SceneMarkerList} />
|
||||
<Route path="/scenes/:id" component={Scene} />
|
||||
</Switch>
|
||||
|
|
|
@ -260,21 +260,18 @@ export class StashService {
|
|||
"allPerformers"
|
||||
];
|
||||
|
||||
public static usePerformerCreate(input: GQL.PerformerCreateInput) {
|
||||
public static usePerformerCreate() {
|
||||
return GQL.usePerformerCreate({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
||||
});
|
||||
}
|
||||
public static usePerformerUpdate(input: GQL.PerformerUpdateInput) {
|
||||
public static usePerformerUpdate() {
|
||||
return GQL.usePerformerUpdate({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
||||
});
|
||||
}
|
||||
public static usePerformerDestroy(input: GQL.PerformerDestroyInput) {
|
||||
public static usePerformerDestroy() {
|
||||
return GQL.usePerformerDestroy({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ interface IListHookOperation {
|
|||
|
||||
export interface IListHookOptions {
|
||||
filterMode: FilterMode;
|
||||
subComponent?: boolean;
|
||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||
props: IBaseProps;
|
||||
zoomable?: boolean;
|
||||
otherOperations?: IListHookOperation[];
|
||||
|
@ -41,15 +43,28 @@ export class ListHook {
|
|||
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
||||
|
||||
// Update the filter when the query parameters change
|
||||
useEffect(() => {
|
||||
const queryParams = queryString.parse(options.props.location.search);
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.configureFromQueryParameters(queryParams);
|
||||
setFilter(newFilter);
|
||||
// don't use query parameters for sub-components
|
||||
if (!options.subComponent) {
|
||||
useEffect(() => {
|
||||
const queryParams = queryString.parse(options.props!.location.search);
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.configureFromQueryParameters(queryParams);
|
||||
setFilter(newFilter);
|
||||
|
||||
// TODO: Need this side effect to update the query params properly
|
||||
filter.configureFromQueryParameters(queryParams);
|
||||
}, [options.props.location.search]);
|
||||
// TODO: Need this side effect to update the query params properly
|
||||
filter.configureFromQueryParameters(queryParams);
|
||||
}, [options.props.location.search]);
|
||||
}
|
||||
|
||||
function getFilter() {
|
||||
if (!options.filterHook) {
|
||||
return filter;
|
||||
}
|
||||
|
||||
// make a copy of the filter and call the hook
|
||||
let newFilter = _.cloneDeep(filter);
|
||||
return options.filterHook(newFilter);
|
||||
}
|
||||
|
||||
let result: QueryHookResult<any, any>;
|
||||
|
||||
|
@ -97,7 +112,7 @@ export class ListHook {
|
|||
}
|
||||
}
|
||||
|
||||
result = getData(filter);
|
||||
result = getData(getFilter());
|
||||
|
||||
useEffect(() => {
|
||||
setTotalCount(getCount());
|
||||
|
@ -108,11 +123,14 @@ export class ListHook {
|
|||
}, [result.data])
|
||||
|
||||
// Update the query parameters when the data changes
|
||||
useEffect(() => {
|
||||
const location = Object.assign({}, options.props.history.location);
|
||||
location.search = filter.makeQueryParameters();
|
||||
options.props.history.replace(location);
|
||||
}, [result.data, filter.displayMode]);
|
||||
// don't use query parameters for sub-components
|
||||
if (!options.subComponent) {
|
||||
useEffect(() => {
|
||||
const location = Object.assign({}, options.props.history.location);
|
||||
location.search = filter.makeQueryParameters();
|
||||
options.props.history.replace(location);
|
||||
}, [result.data, filter.displayMode]);
|
||||
}
|
||||
|
||||
// Update the total count
|
||||
useEffect(() => {
|
||||
|
|
|
@ -446,6 +446,39 @@ span.block {
|
|||
}
|
||||
}
|
||||
|
||||
#performer-page {
|
||||
margin: 10px auto;
|
||||
width: 75%;
|
||||
|
||||
& .details-image-container {
|
||||
max-height: 400px;
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
& .performer-head {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
font-size: 1.2em;
|
||||
|
||||
& .name-icons {
|
||||
margin-left: 10px;
|
||||
|
||||
& .not-favorite .bp3-icon {
|
||||
color: rgba(191, 204, 214, 0.5) !important;
|
||||
}
|
||||
|
||||
& .favorite .bp3-icon {
|
||||
color: #ff7373 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .alias {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
#performer-details {
|
||||
& td {
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -26,8 +26,23 @@ export class EditableTextUtils {
|
|||
value: string | undefined,
|
||||
isEditing: boolean,
|
||||
placeholder?: string,
|
||||
asLabel?: boolean,
|
||||
asURL?: boolean,
|
||||
urlPrefix?: string,
|
||||
onChange: ((value: string) => void),
|
||||
}) {
|
||||
function maybeRenderURL() {
|
||||
if (options.asURL) {
|
||||
let url = options.value;
|
||||
if (options.urlPrefix) {
|
||||
url = options.urlPrefix + url;
|
||||
}
|
||||
return <a href={url}>{options.value}</a>
|
||||
} else {
|
||||
return options.value;
|
||||
}
|
||||
}
|
||||
|
||||
let element: JSX.Element;
|
||||
if (options.isEditing) {
|
||||
element = (
|
||||
|
@ -38,7 +53,11 @@ export class EditableTextUtils {
|
|||
/>
|
||||
);
|
||||
} else {
|
||||
element = <Label>{options.value}</Label>;
|
||||
if (options.asLabel) {
|
||||
element = <Label>{maybeRenderURL()}</Label>;
|
||||
} else {
|
||||
element = <span>{maybeRenderURL()}</span>;
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ export class TableUtils {
|
|||
placeholder?: string,
|
||||
value: string | undefined,
|
||||
isEditing: boolean,
|
||||
asURL?: boolean,
|
||||
urlPrefix?: string,
|
||||
onChange: ((value: string) => void),
|
||||
}) {
|
||||
let optionsCopy = _.clone(options);
|
||||
|
|
Loading…
Reference in New Issue