mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Form-related fixes, improvements and refactoring (#4283)
* Fix another validateDOMNesting error * Fix React.forwardRef error * Fix encoding_image intl message * Return null instead of undefined from RatingSystem * DurationInput tweaks * DateInput tweaks, remove unused utils functions * Refactor and deduplicate edit form rendering * Improve/fix yup validation
This commit is contained in:
@@ -37,7 +37,7 @@
|
|||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"flag-icons": "^6.6.6",
|
"flag-icons": "^6.6.6",
|
||||||
"flexbin": "^0.2.0",
|
"flexbin": "^0.2.0",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.4.5",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"graphql-ws": "^5.11.3",
|
"graphql-ws": "^5.11.3",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"videojs-seek-buttons": "^3.0.1",
|
"videojs-seek-buttons": "^3.0.1",
|
||||||
"videojs-vr": "^2.0.0",
|
"videojs-vr": "^2.0.0",
|
||||||
"videojs-vtt.js": "^0.15.4",
|
"videojs-vtt.js": "^0.15.4",
|
||||||
"yup": "^1.0.0"
|
"yup": "^1.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.12",
|
"@babel/core": "^7.20.12",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { StudioSelect } from "../Shared/Select";
|
import { StudioSelect } from "../Shared/Select";
|
||||||
import { ModalComponent } from "../Shared/Modal";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import { MultiSet } from "../Shared/MultiSet";
|
import { MultiSet } from "../Shared/MultiSet";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
@@ -257,7 +257,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={rating100}
|
value={rating100}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setRating(value ?? undefined)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
|
import { formikUtils } from "src/utils/form";
|
||||||
|
import { yupFormikValidate, yupInputNumber } from "src/utils/yup";
|
||||||
|
|
||||||
interface IGalleryChapterForm {
|
interface IGalleryChapterForm {
|
||||||
galleryID: string;
|
galleryID: string;
|
||||||
@@ -34,11 +36,10 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
|||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().ensure(),
|
title: yup.string().ensure(),
|
||||||
image_index: yup
|
image_index: yupInputNumber()
|
||||||
.number()
|
|
||||||
.integer()
|
.integer()
|
||||||
.required()
|
|
||||||
.moreThan(0)
|
.moreThan(0)
|
||||||
|
.required()
|
||||||
.label(intl.formatMessage({ id: "image_index" })),
|
.label(intl.formatMessage({ id: "image_index" })),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,9 +52,9 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
|||||||
|
|
||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema: schema,
|
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
onSubmit: (values) => onSave(values),
|
validate: yupFormikValidate(schema),
|
||||||
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSave(input: InputValues) {
|
async function onSave(input: InputValues) {
|
||||||
@@ -93,43 +94,25 @@ export const GalleryChapterForm: React.FC<IGalleryChapterForm> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const splitProps = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { renderInputField } = formikUtils(intl, formik, splitProps);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||||
<div>
|
<div className="form-container px-3">
|
||||||
<Form.Group>
|
{renderInputField("title")}
|
||||||
<Form.Label>
|
{renderInputField("image_index", "number")}
|
||||||
<FormattedMessage id="title" />
|
|
||||||
</Form.Label>
|
|
||||||
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "title" })}
|
|
||||||
isInvalid={!!formik.errors.title}
|
|
||||||
{...formik.getFieldProps("title")}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.title}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Label>
|
|
||||||
<FormattedMessage id="image_index" />
|
|
||||||
</Form.Label>
|
|
||||||
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "image_index" })}
|
|
||||||
isInvalid={!!formik.errors.image_index}
|
|
||||||
{...formik.getFieldProps("image_index")}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.image_index}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="buttons-container row">
|
<div className="buttons-container px-3">
|
||||||
<div className="col d-flex">
|
<div className="d-flex">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
disabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
|
|||||||
@@ -25,24 +25,25 @@ import {
|
|||||||
} from "src/components/Shared/Select";
|
} from "src/components/Shared/Select";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { URLListInput } from "src/components/Shared/URLField";
|
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import FormUtils from "src/utils/form";
|
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
|
||||||
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
||||||
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import {
|
import {
|
||||||
Performer,
|
Performer,
|
||||||
PerformerSelect,
|
PerformerSelect,
|
||||||
} from "src/components/Performers/PerformerSelect";
|
} from "src/components/Performers/PerformerSelect";
|
||||||
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
|
import {
|
||||||
|
yupDateString,
|
||||||
|
yupFormikValidate,
|
||||||
|
yupUniqueStringList,
|
||||||
|
} from "src/utils/yup";
|
||||||
|
import { formikUtils } from "src/utils/form";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
@@ -87,7 +88,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
title: titleRequired ? yup.string().required() : yup.string().ensure(),
|
title: titleRequired ? yup.string().required() : yup.string().ensure(),
|
||||||
urls: yupUniqueStringList("urls"),
|
urls: yupUniqueStringList("urls"),
|
||||||
date: yupDateString(intl),
|
date: yupDateString(intl),
|
||||||
rating100: yup.number().nullable().defined(),
|
rating100: yup.number().integer().nullable().defined(),
|
||||||
studio_id: yup.string().required().nullable(),
|
studio_id: yup.string().required().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).defined(),
|
performer_ids: yup.array(yup.string().required()).defined(),
|
||||||
tag_ids: yup.array(yup.string().required()).defined(),
|
tag_ids: yup.array(yup.string().required()).defined(),
|
||||||
@@ -112,8 +113,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validate: yupFormikValidate(schema),
|
||||||
onSubmit: (values) => onSave(values),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
@@ -356,36 +357,109 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
|
||||||
return (
|
|
||||||
<Form.Group controlId={field} as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title,
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
placeholder={placeholder ?? title}
|
|
||||||
{...formik.getFieldProps(field)}
|
|
||||||
isInvalid={!!formik.getFieldMeta(field).error}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.getFieldMeta(field).error}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
const urlsErrors = Array.isArray(formik.errors.urls)
|
const splitProps = {
|
||||||
? formik.errors.urls[0]
|
labelProps: {
|
||||||
: formik.errors.urls;
|
column: true,
|
||||||
const urlsErrorMsg = urlsErrors
|
sm: 3,
|
||||||
? intl.formatMessage({ id: "validation.urls_must_be_unique" })
|
},
|
||||||
: undefined;
|
fieldProps: {
|
||||||
const urlsErrorIdx = urlsErrors?.split(" ").map((e) => parseInt(e));
|
sm: 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const fullWidthProps = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
renderField,
|
||||||
|
renderInputField,
|
||||||
|
renderDateField,
|
||||||
|
renderRatingField,
|
||||||
|
renderURLListField,
|
||||||
|
} = formikUtils(intl, formik, splitProps);
|
||||||
|
|
||||||
|
function renderScenesField() {
|
||||||
|
const title = intl.formatMessage({ id: "scenes" });
|
||||||
|
const control = (
|
||||||
|
<SceneSelect
|
||||||
|
selected={scenes}
|
||||||
|
onSelect={(items) => onSetScenes(items)}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("scene_ids", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStudioField() {
|
||||||
|
const title = intl.formatMessage({ id: "studio" });
|
||||||
|
const control = (
|
||||||
|
<StudioSelect
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"studio_id",
|
||||||
|
items.length > 0 ? items[0]?.id : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("studio_id", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPerformersField() {
|
||||||
|
const title = intl.formatMessage({ id: "performers" });
|
||||||
|
const control = (
|
||||||
|
<PerformerSelect isMulti onSelect={onSetPerformers} values={performers} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("performer_ids", title, control, fullWidthProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagsField() {
|
||||||
|
const title = intl.formatMessage({ id: "tags" });
|
||||||
|
const control = (
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"tag_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.tag_ids}
|
||||||
|
hoverPlacement="right"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("tag_ids", title, control, fullWidthProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetailsField() {
|
||||||
|
const props = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderInputField("details", "textarea", "details", props);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="gallery-edit-details">
|
<div id="gallery-edit-details">
|
||||||
@@ -396,8 +470,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
{maybeRenderScrapeDialog()}
|
{maybeRenderScrapeDialog()}
|
||||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||||
<div className="form-container row px-3 pt-3">
|
<Row className="form-container edit-buttons-container px-3 pt-3">
|
||||||
<div className="col edit-buttons mb-3 pl-0">
|
<div className="edit-buttons mb-3 pl-0">
|
||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -416,148 +490,31 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
<FormattedMessage id="actions.delete" />
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Col xs={6} className="text-right">
|
<div className="ml-auto text-right d-flex">{renderScraperMenu()}</div>
|
||||||
{renderScraperMenu()}
|
</Row>
|
||||||
</Col>
|
<Row className="form-container px-3">
|
||||||
</div>
|
<Col lg={7} xl={12}>
|
||||||
<div className="form-container row px-3">
|
{renderInputField("title")}
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
|
||||||
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
|
||||||
<Form.Group controlId="urls" as={Row}>
|
|
||||||
<Col xs={3} className="pr-0 url-label">
|
|
||||||
<Form.Label className="col-form-label">
|
|
||||||
<FormattedMessage id="urls" />
|
|
||||||
</Form.Label>
|
|
||||||
</Col>
|
|
||||||
<Col xs={9}>
|
|
||||||
<URLListInput
|
|
||||||
value={formik.values.urls ?? []}
|
|
||||||
setValue={(value) => formik.setFieldValue("urls", value)}
|
|
||||||
errors={urlsErrorMsg}
|
|
||||||
errorIdx={urlsErrorIdx}
|
|
||||||
onScrapeClick={(url) => onScrapeGalleryURL(url)}
|
|
||||||
urlScrapable={urlScrapable}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="date" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "date" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<DateInput
|
|
||||||
value={formik.values.date}
|
|
||||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
|
||||||
error={formik.errors.date}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="rating" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "rating" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<RatingSystem
|
|
||||||
value={formik.values.rating100 ?? undefined}
|
|
||||||
onSetRating={(value) =>
|
|
||||||
formik.setFieldValue("rating100", value ?? null)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<StudioSelect
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"studio_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="performers" as={Row}>
|
{renderURLListField(
|
||||||
{FormUtils.renderLabel({
|
"urls",
|
||||||
title: intl.formatMessage({ id: "performers" }),
|
"validation.urls_must_be_unique",
|
||||||
labelProps: {
|
onScrapeGalleryURL,
|
||||||
column: true,
|
urlScrapable
|
||||||
sm: 3,
|
)}
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<PerformerSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={onSetPerformers}
|
|
||||||
values={performers}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="tags" as={Row}>
|
{renderDateField("date")}
|
||||||
{FormUtils.renderLabel({
|
{renderRatingField("rating100", "rating")}
|
||||||
title: intl.formatMessage({ id: "tags" }),
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<TagSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"tag_ids",
|
|
||||||
items.map((item) => item.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.tag_ids}
|
|
||||||
hoverPlacement="right"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="scenes" as={Row}>
|
{renderScenesField()}
|
||||||
{FormUtils.renderLabel({
|
{renderStudioField()}
|
||||||
title: intl.formatMessage({ id: "scenes" }),
|
{renderPerformersField()}
|
||||||
labelProps: {
|
{renderTagsField()}
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<SceneSelect
|
|
||||||
selected={scenes}
|
|
||||||
onSelect={(items) => onSetScenes(items)}
|
|
||||||
isMulti
|
|
||||||
/>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
<Col lg={5} xl={12}>
|
||||||
</div>
|
{renderDetailsField()}
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
</Col>
|
||||||
<Form.Group controlId="details">
|
</Row>
|
||||||
<Form.Label>
|
|
||||||
<FormattedMessage id="details" />
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="gallery-description text-input"
|
|
||||||
onChange={(e) =>
|
|
||||||
formik.setFieldValue("details", e.currentTarget.value)
|
|
||||||
}
|
|
||||||
value={formik.values.details}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<RatingSystem value={gallery.rating100 ?? undefined} disabled />
|
<RatingSystem value={gallery.rating100} disabled />
|
||||||
<img loading="lazy" src={cover} alt="" className={CLASSNAME_IMG} />
|
<img loading="lazy" src={cover} alt="" className={CLASSNAME_IMG} />
|
||||||
<footer className={CLASSNAME_FOOTER}>
|
<footer className={CLASSNAME_FOOTER}>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -15,10 +15,6 @@
|
|||||||
.tab-content {
|
.tab-content {
|
||||||
min-height: 15rem;
|
min-height: 15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-description {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-card {
|
.gallery-card {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { StudioSelect } from "src/components/Shared/Select";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import { ModalComponent } from "src/components/Shared/Modal";
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import { MultiSet } from "../Shared/MultiSet";
|
import { MultiSet } from "../Shared/MultiSet";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
@@ -247,7 +247,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={rating100}
|
value={rating100}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setRating(value ?? undefined)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -6,21 +6,22 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { TagSelect, StudioSelect } from "src/components/Shared/Select";
|
import { TagSelect, StudioSelect } from "src/components/Shared/Select";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { URLListInput } from "src/components/Shared/URLField";
|
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import FormUtils from "src/utils/form";
|
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
|
||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
import {
|
||||||
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
|
yupDateString,
|
||||||
|
yupFormikValidate,
|
||||||
|
yupUniqueStringList,
|
||||||
|
} from "src/utils/yup";
|
||||||
import {
|
import {
|
||||||
Performer,
|
Performer,
|
||||||
PerformerSelect,
|
PerformerSelect,
|
||||||
} from "src/components/Performers/PerformerSelect";
|
} from "src/components/Performers/PerformerSelect";
|
||||||
|
import { formikUtils } from "src/utils/form";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
@@ -49,7 +50,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
title: yup.string().ensure(),
|
title: yup.string().ensure(),
|
||||||
urls: yupUniqueStringList("urls"),
|
urls: yupUniqueStringList("urls"),
|
||||||
date: yupDateString(intl),
|
date: yupDateString(intl),
|
||||||
rating100: yup.number().nullable().defined(),
|
rating100: yup.number().integer().nullable().defined(),
|
||||||
studio_id: yup.string().required().nullable(),
|
studio_id: yup.string().required().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).defined(),
|
performer_ids: yup.array(yup.string().required()).defined(),
|
||||||
tag_ids: yup.array(yup.string().required()).defined(),
|
tag_ids: yup.array(yup.string().required()).defined(),
|
||||||
@@ -70,8 +71,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validate: yupFormikValidate(schema),
|
||||||
onSubmit: (values) => onSave(values),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
@@ -128,36 +129,80 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
|
||||||
return (
|
|
||||||
<Form.Group controlId={field} as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title,
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
placeholder={placeholder ?? title}
|
|
||||||
{...formik.getFieldProps(field)}
|
|
||||||
isInvalid={!!formik.getFieldMeta(field).error}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.getFieldMeta(field).error}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
const urlsErrors = Array.isArray(formik.errors.urls)
|
const splitProps = {
|
||||||
? formik.errors.urls[0]
|
labelProps: {
|
||||||
: formik.errors.urls;
|
column: true,
|
||||||
const urlsErrorMsg = urlsErrors
|
sm: 3,
|
||||||
? intl.formatMessage({ id: "validation.urls_must_be_unique" })
|
},
|
||||||
: undefined;
|
fieldProps: {
|
||||||
const urlsErrorIdx = urlsErrors?.split(" ").map((e) => parseInt(e));
|
sm: 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const fullWidthProps = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
renderField,
|
||||||
|
renderInputField,
|
||||||
|
renderDateField,
|
||||||
|
renderRatingField,
|
||||||
|
renderURLListField,
|
||||||
|
} = formikUtils(intl, formik, splitProps);
|
||||||
|
|
||||||
|
function renderStudioField() {
|
||||||
|
const title = intl.formatMessage({ id: "studio" });
|
||||||
|
const control = (
|
||||||
|
<StudioSelect
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"studio_id",
|
||||||
|
items.length > 0 ? items[0]?.id : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("studio_id", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPerformersField() {
|
||||||
|
const title = intl.formatMessage({ id: "performers" });
|
||||||
|
const control = (
|
||||||
|
<PerformerSelect isMulti onSelect={onSetPerformers} values={performers} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("performer_ids", title, control, fullWidthProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagsField() {
|
||||||
|
const title = intl.formatMessage({ id: "tags" });
|
||||||
|
const control = (
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"tag_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.tag_ids}
|
||||||
|
hoverPlacement="right"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("tag_ids", title, control, fullWidthProps);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="image-edit-details">
|
<div id="image-edit-details">
|
||||||
@@ -167,8 +212,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||||
<div className="form-container row px-3 pt-3">
|
<Row className="form-container edit-buttons-container px-3 pt-3">
|
||||||
<div className="col edit-buttons mb-3 pl-0">
|
<div className="edit-buttons mb-3 pl-0">
|
||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -185,110 +230,21 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
<FormattedMessage id="actions.delete" />
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Row>
|
||||||
<div className="form-container row px-3">
|
<Row className="form-container px-3">
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<Col lg={7} xl={12}>
|
||||||
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
{renderInputField("title")}
|
||||||
<Form.Group controlId="urls" as={Row}>
|
|
||||||
<Col xs={3} className="pr-0 url-label">
|
|
||||||
<Form.Label className="col-form-label">
|
|
||||||
<FormattedMessage id="urls" />
|
|
||||||
</Form.Label>
|
|
||||||
</Col>
|
|
||||||
<Col xs={9}>
|
|
||||||
<URLListInput
|
|
||||||
value={formik.values.urls ?? []}
|
|
||||||
setValue={(value) => formik.setFieldValue("urls", value)}
|
|
||||||
errors={urlsErrorMsg}
|
|
||||||
errorIdx={urlsErrorIdx}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="date" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "date" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<DateInput
|
|
||||||
value={formik.values.date}
|
|
||||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
|
||||||
error={formik.errors.date}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="rating" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "rating" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<RatingSystem
|
|
||||||
value={formik.values.rating100 ?? undefined}
|
|
||||||
onSetRating={(value) =>
|
|
||||||
formik.setFieldValue("rating100", value ?? null)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<StudioSelect
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"studio_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="performers" as={Row}>
|
{renderURLListField("urls", "validation.urls_must_be_unique")}
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "performers" }),
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<PerformerSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={onSetPerformers}
|
|
||||||
values={performers}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="tags" as={Row}>
|
{renderDateField("date")}
|
||||||
{FormUtils.renderLabel({
|
{renderRatingField("rating100", "rating")}
|
||||||
title: intl.formatMessage({ id: "tags" }),
|
|
||||||
labelProps: {
|
{renderStudioField()}
|
||||||
column: true,
|
{renderPerformersField()}
|
||||||
sm: 3,
|
{renderTagsField()}
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<TagSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"tag_ids",
|
|
||||||
items.map((item) => item.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.tag_ids}
|
|
||||||
hoverPlacement="right"
|
|
||||||
/>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Row>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export const DurationFilter: React.FC<IDurationFilterProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
function onChanged(v: number | undefined, property: "value" | "value2") {
|
function onChanged(v: number | null, property: "value" | "value2") {
|
||||||
const { value } = criterion;
|
const { value } = criterion;
|
||||||
value[property] = v;
|
value[property] = v ?? undefined;
|
||||||
onValueChanged(value);
|
onValueChanged(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { ModalComponent } from "../Shared/Modal";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { StudioSelect } from "../Shared/Select";
|
import { StudioSelect } from "../Shared/Select";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
@@ -127,7 +127,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={rating100}
|
value={rating100}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setRating(value ?? undefined)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
<div className="logo w-100">
|
<div className="logo w-100">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="movie-images">
|
<div className="movie-images">
|
||||||
@@ -423,8 +423,8 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
</h2>
|
</h2>
|
||||||
{maybeRenderAliases()}
|
{maybeRenderAliases()}
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={movie.rating100 ?? undefined}
|
value={movie.rating100}
|
||||||
onSetRating={(value) => setRating(value ?? null)}
|
onSetRating={(value) => setRating(value)}
|
||||||
/>
|
/>
|
||||||
{maybeRenderDetails()}
|
{maybeRenderDetails()}
|
||||||
{maybeRenderEditPanel()}
|
{maybeRenderEditPanel()}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const MovieCreate: React.FC = () => {
|
|||||||
<div className="logo w-100">
|
<div className="logo w-100">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="movie-images">
|
<div className="movie-images">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import DurationUtils from "src/utils/duration";
|
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { DetailItem } from "src/components/Shared/DetailItem";
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
|
|||||||
<DetailItem
|
<DetailItem
|
||||||
id="duration"
|
id="duration"
|
||||||
value={
|
value={
|
||||||
movie.duration ? DurationUtils.secondsToString(movie.duration) : ""
|
movie.duration ? TextUtils.secondsToTimestamp(movie.duration) : ""
|
||||||
}
|
}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ import {
|
|||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { StudioSelect } from "src/components/Shared/Select";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { DurationInput } from "src/components/Shared/DurationInput";
|
|
||||||
import { URLField } from "src/components/Shared/URLField";
|
import { URLField } from "src/components/Shared/URLField";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap";
|
import { Modal as BSModal, Form, Button } from "react-bootstrap";
|
||||||
import DurationUtils from "src/utils/duration";
|
import TextUtils from "src/utils/text";
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
|
import { formikUtils } from "src/utils/form";
|
||||||
|
import { yupDateString, yupFormikValidate } from "src/utils/yup";
|
||||||
|
|
||||||
interface IMovieEditPanel {
|
interface IMovieEditPanel {
|
||||||
movie: Partial<GQL.MovieDataFragment>;
|
movie: Partial<GQL.MovieDataFragment>;
|
||||||
@@ -55,28 +55,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
const Scrapers = useListMovieScrapers();
|
const Scrapers = useListMovieScrapers();
|
||||||
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
||||||
|
|
||||||
const labelXS = 3;
|
|
||||||
const labelXL = 2;
|
|
||||||
const fieldXS = 9;
|
|
||||||
const fieldXL = 7;
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
aliases: yup.string().ensure(),
|
aliases: yup.string().ensure(),
|
||||||
duration: yup.number().nullable().defined(),
|
duration: yup.number().integer().min(0).nullable().defined(),
|
||||||
date: yup
|
date: yupDateString(intl),
|
||||||
.string()
|
|
||||||
.ensure()
|
|
||||||
.test({
|
|
||||||
name: "date",
|
|
||||||
test: (value) => {
|
|
||||||
if (!value) return true;
|
|
||||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
|
||||||
if (Number.isNaN(Date.parse(value))) return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
|
||||||
}),
|
|
||||||
studio_id: yup.string().required().nullable(),
|
studio_id: yup.string().required().nullable(),
|
||||||
director: yup.string().ensure(),
|
director: yup.string().ensure(),
|
||||||
url: yup.string().ensure(),
|
url: yup.string().ensure(),
|
||||||
@@ -101,8 +84,8 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validate: yupFormikValidate(schema),
|
||||||
onSubmit: (values) => onSave(values),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
@@ -135,8 +118,8 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.duration) {
|
if (state.duration) {
|
||||||
const seconds = DurationUtils.stringToSeconds(state.duration);
|
const seconds = TextUtils.timestampToSeconds(state.duration);
|
||||||
if (seconds !== undefined) {
|
if (seconds) {
|
||||||
formik.setFieldValue("duration", seconds);
|
formik.setFieldValue("duration", seconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,27 +313,41 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
const isEditing = true;
|
const {
|
||||||
|
renderField,
|
||||||
|
renderInputField,
|
||||||
|
renderDateField,
|
||||||
|
renderDurationField,
|
||||||
|
} = formikUtils(intl, formik);
|
||||||
|
|
||||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
function renderStudioField() {
|
||||||
return (
|
const title = intl.formatMessage({ id: "studio" });
|
||||||
<Form.Group controlId={field} as={Row}>
|
const control = (
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<StudioSelect
|
||||||
<FormattedMessage id={title} />
|
onSelect={(items) =>
|
||||||
</Form.Label>
|
formik.setFieldValue(
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
"studio_id",
|
||||||
<Form.Control
|
items.length > 0 ? items[0]?.id : null
|
||||||
className="text-input"
|
)
|
||||||
placeholder={placeholder ?? title}
|
}
|
||||||
{...formik.getFieldProps(field)}
|
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
||||||
isInvalid={!!formik.getFieldMeta(field).error}
|
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.getFieldMeta(field).error}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return renderField("studio_id", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUrlField() {
|
||||||
|
const title = intl.formatMessage({ id: "url" });
|
||||||
|
const control = (
|
||||||
|
<URLField
|
||||||
|
{...formik.getFieldProps("url")}
|
||||||
|
onScrapeClick={onScrapeMovieURL}
|
||||||
|
urlScrapable={urlScrapable}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("url", title, control);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: CSS class
|
// TODO: CSS class
|
||||||
@@ -377,102 +374,21 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Form noValidate onSubmit={formik.handleSubmit} id="movie-edit">
|
<Form noValidate onSubmit={formik.handleSubmit} id="movie-edit">
|
||||||
<Form.Group controlId="name" as={Row}>
|
{renderInputField("name")}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
{renderInputField("aliases")}
|
||||||
<FormattedMessage id="name" />
|
{renderDurationField("duration")}
|
||||||
</Form.Label>
|
{renderDateField("date")}
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
{renderStudioField()}
|
||||||
<Form.Control
|
{renderInputField("director")}
|
||||||
className="text-input"
|
{renderUrlField()}
|
||||||
placeholder={intl.formatMessage({ id: "name" })}
|
{renderInputField("synopsis", "textarea")}
|
||||||
{...formik.getFieldProps("name")}
|
|
||||||
isInvalid={!!formik.errors.name}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.name}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderTextField("aliases", intl.formatMessage({ id: "aliases" }))}
|
|
||||||
|
|
||||||
<Form.Group controlId="duration" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="duration" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<DurationInput
|
|
||||||
value={formik.values.duration ?? undefined}
|
|
||||||
setValue={(v) => formik.setFieldValue("duration", v ?? null)}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="date" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="date" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<DateInput
|
|
||||||
value={formik.values.date}
|
|
||||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
|
||||||
error={formik.errors.date}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="studio" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<StudioSelect
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"studio_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderTextField("director", intl.formatMessage({ id: "director" }))}
|
|
||||||
|
|
||||||
<Form.Group controlId="url" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="url" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<URLField
|
|
||||||
{...formik.getFieldProps("url")}
|
|
||||||
onScrapeClick={onScrapeMovieURL}
|
|
||||||
urlScrapable={urlScrapable}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="synopsis" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="synopsis" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "synopsis" })}
|
|
||||||
{...formik.getFieldProps("synopsis")}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<DetailsEditNavbar
|
<DetailsEditNavbar
|
||||||
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
|
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
classNames="col-xl-9 mt-3"
|
classNames="col-xl-9 mt-3"
|
||||||
isEditing={isEditing}
|
isEditing
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
onSave={formik.handleSubmit}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ScrapedTextAreaRow,
|
ScrapedTextAreaRow,
|
||||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||||
import { StudioSelect } from "src/components/Shared/Select";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import DurationUtils from "src/utils/duration";
|
import TextUtils from "src/utils/text";
|
||||||
import { useStudioCreate } from "src/core/StashService";
|
import { useStudioCreate } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||||
@@ -83,10 +83,10 @@ export const MovieScrapeDialog: React.FC<IMovieScrapeDialogProps> = (
|
|||||||
);
|
);
|
||||||
const [duration, setDuration] = useState<ScrapeResult<string>>(
|
const [duration, setDuration] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(
|
new ScrapeResult<string>(
|
||||||
DurationUtils.secondsToString(props.movie.duration || 0),
|
TextUtils.secondsToTimestamp(props.movie.duration || 0),
|
||||||
// convert seconds to string if it's a number
|
// convert seconds to string if it's a number
|
||||||
props.scraped.duration && !isNaN(+props.scraped.duration)
|
props.scraped.duration && !isNaN(+props.scraped.duration)
|
||||||
? DurationUtils.secondsToString(parseInt(props.scraped.duration, 10))
|
? TextUtils.secondsToTimestamp(parseInt(props.scraped.duration, 10))
|
||||||
: props.scraped.duration
|
: props.scraped.duration
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimPerformerDataFragment[];
|
selected: GQL.SlimPerformerDataFragment[];
|
||||||
@@ -245,8 +245,10 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={updateInput.rating100 ?? undefined}
|
value={updateInput.rating100}
|
||||||
onSetRating={(value) => setUpdateField({ rating100: value })}
|
onSetRating={(value) =>
|
||||||
|
setUpdateField({ rating100: value ?? undefined })
|
||||||
|
}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -573,7 +573,7 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
|||||||
<div className="detail-header-image">
|
<div className="detail-header-image">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
@@ -593,8 +593,8 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
|||||||
</h2>
|
</h2>
|
||||||
{maybeRenderAliases()}
|
{maybeRenderAliases()}
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={performer.rating100 ?? undefined}
|
value={performer.rating100}
|
||||||
onSetRating={(value) => setRating(value ?? null)}
|
onSetRating={(value) => setRating(value)}
|
||||||
/>
|
/>
|
||||||
{maybeRenderDetails()}
|
{maybeRenderDetails()}
|
||||||
{maybeRenderEditPanel()}
|
{maybeRenderEditPanel()}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const PerformerCreate: React.FC = () => {
|
|||||||
if (encodingImage) {
|
if (encodingImage) {
|
||||||
return (
|
return (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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, Badge, Dropdown } from "react-bootstrap";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } 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";
|
||||||
@@ -39,15 +39,16 @@ import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
|||||||
import PerformerScrapeModal from "./PerformerScrapeModal";
|
import PerformerScrapeModal from "./PerformerScrapeModal";
|
||||||
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
|
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import {
|
import { faPlus, faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
faPlus,
|
|
||||||
faSyncAlt,
|
|
||||||
faTrashAlt,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { StashIDPill } from "src/components/Shared/StashID";
|
import {
|
||||||
|
yupFormikValidate,
|
||||||
|
yupInputNumber,
|
||||||
|
yupInputEnum,
|
||||||
|
yupDateString,
|
||||||
|
yupUniqueAliases,
|
||||||
|
} from "src/utils/yup";
|
||||||
|
|
||||||
const isScraper = (
|
const isScraper = (
|
||||||
scraper: GQL.Scraper | GQL.StashBox
|
scraper: GQL.Scraper | GQL.StashBox
|
||||||
@@ -92,71 +93,23 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
const [createTag] = useTagCreate();
|
const [createTag] = useTagCreate();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const labelXS = 3;
|
|
||||||
const labelXL = 2;
|
|
||||||
const fieldXS = 9;
|
|
||||||
const fieldXL = 7;
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
disambiguation: yup.string().ensure(),
|
disambiguation: yup.string().ensure(),
|
||||||
alias_list: yup
|
alias_list: yupUniqueAliases("alias_list", "name"),
|
||||||
.array(yup.string().required())
|
gender: yupInputEnum(GQL.GenderEnum).nullable().defined(),
|
||||||
.defined()
|
birthdate: yupDateString(intl),
|
||||||
.test({
|
death_date: yupDateString(intl),
|
||||||
name: "unique",
|
|
||||||
test: (value, context) => {
|
|
||||||
const aliases = [context.parent.name.toLowerCase()];
|
|
||||||
const dupes: number[] = [];
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
|
||||||
const a = value[i].toLowerCase();
|
|
||||||
if (aliases.includes(a)) {
|
|
||||||
dupes.push(i);
|
|
||||||
} else {
|
|
||||||
aliases.push(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dupes.length === 0) return true;
|
|
||||||
return new yup.ValidationError(dupes.join(" "), value, "alias_list");
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
gender: yup.string<GQL.GenderEnum | "">().ensure(),
|
|
||||||
birthdate: yup
|
|
||||||
.string()
|
|
||||||
.ensure()
|
|
||||||
.test({
|
|
||||||
name: "date",
|
|
||||||
test: (value) => {
|
|
||||||
if (!value) return true;
|
|
||||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
|
||||||
if (Number.isNaN(Date.parse(value))) return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
|
||||||
}),
|
|
||||||
death_date: yup
|
|
||||||
.string()
|
|
||||||
.ensure()
|
|
||||||
.test({
|
|
||||||
name: "date",
|
|
||||||
test: (value) => {
|
|
||||||
if (!value) return true;
|
|
||||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
|
||||||
if (Number.isNaN(Date.parse(value))) return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
|
||||||
}),
|
|
||||||
country: yup.string().ensure(),
|
country: yup.string().ensure(),
|
||||||
ethnicity: yup.string().ensure(),
|
ethnicity: yup.string().ensure(),
|
||||||
hair_color: yup.string().ensure(),
|
hair_color: yup.string().ensure(),
|
||||||
eye_color: yup.string().ensure(),
|
eye_color: yup.string().ensure(),
|
||||||
height_cm: yup.number().nullable().defined().default(null),
|
height_cm: yupInputNumber().positive().truncate().nullable().defined(),
|
||||||
weight: yup.number().nullable().defined().default(null),
|
weight: yupInputNumber().positive().truncate().nullable().defined(),
|
||||||
measurements: yup.string().ensure(),
|
measurements: yup.string().ensure(),
|
||||||
fake_tits: yup.string().ensure(),
|
fake_tits: yup.string().ensure(),
|
||||||
penis_length: yup.number().nullable().defined().default(null),
|
penis_length: yupInputNumber().positive().truncate().nullable().defined(),
|
||||||
circumcised: yup.string<GQL.CircumisedEnum | "">().ensure(),
|
circumcised: yupInputEnum(GQL.CircumisedEnum).nullable().defined(),
|
||||||
tattoos: yup.string().ensure(),
|
tattoos: yup.string().ensure(),
|
||||||
piercings: yup.string().ensure(),
|
piercings: yup.string().ensure(),
|
||||||
career_length: yup.string().ensure(),
|
career_length: yup.string().ensure(),
|
||||||
@@ -174,7 +127,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
name: performer.name ?? "",
|
name: performer.name ?? "",
|
||||||
disambiguation: performer.disambiguation ?? "",
|
disambiguation: performer.disambiguation ?? "",
|
||||||
alias_list: performer.alias_list ?? [],
|
alias_list: performer.alias_list ?? [],
|
||||||
gender: (performer.gender as GQL.GenderEnum) ?? "",
|
gender: performer.gender ?? null,
|
||||||
birthdate: performer.birthdate ?? "",
|
birthdate: performer.birthdate ?? "",
|
||||||
death_date: performer.death_date ?? "",
|
death_date: performer.death_date ?? "",
|
||||||
country: performer.country ?? "",
|
country: performer.country ?? "",
|
||||||
@@ -186,7 +139,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
measurements: performer.measurements ?? "",
|
measurements: performer.measurements ?? "",
|
||||||
fake_tits: performer.fake_tits ?? "",
|
fake_tits: performer.fake_tits ?? "",
|
||||||
penis_length: performer.penis_length ?? null,
|
penis_length: performer.penis_length ?? null,
|
||||||
circumcised: (performer.circumcised as GQL.CircumisedEnum) ?? "",
|
circumcised: performer.circumcised ?? null,
|
||||||
tattoos: performer.tattoos ?? "",
|
tattoos: performer.tattoos ?? "",
|
||||||
piercings: performer.piercings ?? "",
|
piercings: performer.piercings ?? "",
|
||||||
career_length: performer.career_length ?? "",
|
career_length: performer.career_length ?? "",
|
||||||
@@ -204,8 +157,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validate: yupFormikValidate(schema),
|
||||||
onSubmit: (values) => onSave(values),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
function translateScrapedGender(scrapedGender?: string) {
|
function translateScrapedGender(scrapedGender?: string) {
|
||||||
@@ -240,42 +193,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNewTags() {
|
|
||||||
if (!newTags || newTags.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = (
|
|
||||||
<>
|
|
||||||
{newTags.map((t) => (
|
|
||||||
<Badge
|
|
||||||
className="tag-item"
|
|
||||||
variant="secondary"
|
|
||||||
key={t.name}
|
|
||||||
onClick={() => createNewTag(t)}
|
|
||||||
>
|
|
||||||
{t.name}
|
|
||||||
<Button className="minimal ml-2">
|
|
||||||
<Icon className="fa-fw" icon={faPlus} />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const minCollapseLength = 10;
|
|
||||||
|
|
||||||
if (newTags.length >= minCollapseLength) {
|
|
||||||
return (
|
|
||||||
<CollapseButton text={`Missing (${newTags.length})`}>
|
|
||||||
{ret}
|
|
||||||
</CollapseButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createNewTag(toCreate: GQL.ScrapedTag) {
|
async function createNewTag(toCreate: GQL.ScrapedTag) {
|
||||||
const tagInput: GQL.TagCreateInput = { name: toCreate.name ?? "" };
|
const tagInput: GQL.TagCreateInput = { name: toCreate.name ?? "" };
|
||||||
try {
|
try {
|
||||||
@@ -451,21 +368,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
function valuesToInput(input: InputValues): GQL.PerformerCreateInput {
|
|
||||||
return {
|
|
||||||
...input,
|
|
||||||
gender: input.gender || null,
|
|
||||||
height_cm: input.height_cm || null,
|
|
||||||
weight: input.weight || null,
|
|
||||||
penis_length: input.penis_length || null,
|
|
||||||
circumcised: input.circumcised || null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave(input: InputValues) {
|
async function onSave(input: InputValues) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await onSubmit(valuesToInput(input));
|
await onSubmit(input);
|
||||||
formik.resetForm();
|
formik.resetForm();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -668,7 +574,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentPerformer = {
|
const currentPerformer = {
|
||||||
...valuesToInput(formik.values),
|
...formik.values,
|
||||||
image: formik.values.image ?? performer.image_path,
|
image: formik.values.image ?? performer.image_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -746,13 +652,81 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
) : undefined;
|
) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderTagsField() {
|
const {
|
||||||
|
renderField,
|
||||||
|
renderInputField,
|
||||||
|
renderSelectField,
|
||||||
|
renderDateField,
|
||||||
|
renderStringListField,
|
||||||
|
renderStashIDsField,
|
||||||
|
} = formikUtils(intl, formik);
|
||||||
|
|
||||||
|
function renderCountryField() {
|
||||||
|
const title = intl.formatMessage({ id: "country" });
|
||||||
|
const control = (
|
||||||
|
<CountrySelect
|
||||||
|
value={formik.values.country}
|
||||||
|
onChange={(v) => formik.setFieldValue("country", v)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("country", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUrlField() {
|
||||||
|
const title = intl.formatMessage({ id: "url" });
|
||||||
|
const control = (
|
||||||
|
<URLField
|
||||||
|
{...formik.getFieldProps("url")}
|
||||||
|
onScrapeClick={onScrapePerformerURL}
|
||||||
|
urlScrapable={urlScrapable}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("url", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNewTags() {
|
||||||
|
if (!newTags || newTags.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = (
|
||||||
|
<>
|
||||||
|
{newTags.map((t) => (
|
||||||
|
<Badge
|
||||||
|
className="tag-item"
|
||||||
|
variant="secondary"
|
||||||
|
key={t.name}
|
||||||
|
onClick={() => createNewTag(t)}
|
||||||
|
>
|
||||||
|
{t.name}
|
||||||
|
<Button className="minimal ml-2">
|
||||||
|
<Icon className="fa-fw" icon={faPlus} />
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const minCollapseLength = 10;
|
||||||
|
|
||||||
|
if (newTags.length >= minCollapseLength) {
|
||||||
return (
|
return (
|
||||||
<Form.Group controlId="tags" as={Row}>
|
<CollapseButton text={`Missing (${newTags.length})`}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
{ret}
|
||||||
<FormattedMessage id="tags" defaultMessage="Tags" />
|
</CollapseButton>
|
||||||
</Form.Label>
|
);
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagsField() {
|
||||||
|
const title = intl.formatMessage({ id: "tags" });
|
||||||
|
|
||||||
|
const control = (
|
||||||
|
<>
|
||||||
<TagSelect
|
<TagSelect
|
||||||
menuPortalTarget={document.body}
|
menuPortalTarget={document.body}
|
||||||
isMulti
|
isMulti
|
||||||
@@ -765,93 +739,12 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
ids={formik.values.tag_ids}
|
ids={formik.values.tag_ids}
|
||||||
/>
|
/>
|
||||||
{renderNewTags()}
|
{renderNewTags()}
|
||||||
</Col>
|
</>
|
||||||
</Form.Group>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return renderField("tag_ids", title, control);
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
|
||||||
formik.setFieldValue(
|
|
||||||
"stash_ids",
|
|
||||||
(formik.values.stash_ids ?? []).filter(
|
|
||||||
(s) =>
|
|
||||||
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderStashIDs() {
|
|
||||||
if (!formik.values.stash_ids?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
|
||||||
{intl.formatMessage({ id: "stash_ids" })}
|
|
||||||
</Form.Label>
|
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
|
||||||
<ul className="pl-0">
|
|
||||||
{formik.values.stash_ids.map((stashID) => {
|
|
||||||
return (
|
|
||||||
<li key={stashID.stash_id} className="row no-gutters mb-1">
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
className="mr-2 py-0"
|
|
||||||
title={intl.formatMessage({ id: "actions.delete_stashid" })}
|
|
||||||
onClick={() => removeStashID(stashID)}
|
|
||||||
>
|
|
||||||
<Icon icon={faTrashAlt} />
|
|
||||||
</Button>
|
|
||||||
<StashIDPill stashID={stashID} linkType="performers" />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderField(
|
|
||||||
field: string,
|
|
||||||
props?: {
|
|
||||||
messageID?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const title = intl.formatMessage({ id: props?.messageID ?? field });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Group controlId={field} as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
{title}
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
type={props?.type ?? "text"}
|
|
||||||
className="text-input"
|
|
||||||
placeholder={props?.placeholder ?? title}
|
|
||||||
{...formik.getFieldProps(field)}
|
|
||||||
isInvalid={!!formik.getFieldMeta(field).error}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.getFieldMeta(field).error}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const aliasErrors = Array.isArray(formik.errors.alias_list)
|
|
||||||
? formik.errors.alias_list[0]
|
|
||||||
: formik.errors.alias_list;
|
|
||||||
const aliasErrorMsg = aliasErrors
|
|
||||||
? intl.formatMessage({ id: "validation.aliases_must_be_unique" })
|
|
||||||
: undefined;
|
|
||||||
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderScrapeModal()}
|
{renderScrapeModal()}
|
||||||
@@ -864,231 +757,51 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
{renderButtons("mb-3")}
|
{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}>
|
{renderInputField("name")}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
{renderInputField("disambiguation")}
|
||||||
<FormattedMessage id="name" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "name" })}
|
|
||||||
{...formik.getFieldProps("name")}
|
|
||||||
isInvalid={!!formik.errors.name}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.name}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="disambiguation" as={Row}>
|
{renderStringListField(
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
"alias_list",
|
||||||
<FormattedMessage id="disambiguation" />
|
"validation.aliases_must_be_unique",
|
||||||
</Form.Label>
|
"aliases"
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
)}
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "disambiguation" })}
|
|
||||||
{...formik.getFieldProps("disambiguation")}
|
|
||||||
isInvalid={!!formik.errors.disambiguation}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.disambiguation}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="aliases" as={Row}>
|
{renderSelectField("gender", stringGenderMap)}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="aliases" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<StringListInput
|
|
||||||
value={formik.values.alias_list ?? []}
|
|
||||||
setValue={(value) => formik.setFieldValue("alias_list", value)}
|
|
||||||
errors={aliasErrorMsg}
|
|
||||||
errorIdx={aliasErrorIdx}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group as={Row}>
|
{renderDateField("birthdate")}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
{renderDateField("death_date")}
|
||||||
<FormattedMessage id="gender" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs="auto">
|
|
||||||
<Form.Control
|
|
||||||
as="select"
|
|
||||||
className="input-control"
|
|
||||||
{...formik.getFieldProps("gender")}
|
|
||||||
>
|
|
||||||
<option value="" key=""></option>
|
|
||||||
{Array.from(stringGenderMap.entries()).map(([name, value]) => (
|
|
||||||
<option value={value} key={value}>
|
|
||||||
{name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="birthdate" as={Row}>
|
{renderCountryField()}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="birthdate" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<DateInput
|
|
||||||
value={formik.values.birthdate}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
formik.setFieldValue("birthdate", value)
|
|
||||||
}
|
|
||||||
error={formik.errors.birthdate}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="death_date" as={Row}>
|
{renderInputField("ethnicity")}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
{renderInputField("hair_color")}
|
||||||
<FormattedMessage id="death_date" />
|
{renderInputField("eye_color")}
|
||||||
</Form.Label>
|
{renderInputField("height_cm", "number")}
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
{renderInputField("weight", "number", "weight_kg")}
|
||||||
<DateInput
|
{renderInputField("penis_length", "number", "penis_length_cm")}
|
||||||
value={formik.values.death_date}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
formik.setFieldValue("death_date", value)
|
|
||||||
}
|
|
||||||
error={formik.errors.death_date}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group as={Row}>
|
{renderSelectField("circumcised", stringCircumMap)}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="country" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<CountrySelect
|
|
||||||
value={formik.getFieldProps("country").value}
|
|
||||||
onChange={(value) => formik.setFieldValue("country", value)}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderField("ethnicity")}
|
{renderInputField("measurements")}
|
||||||
{renderField("hair_color")}
|
{renderInputField("fake_tits")}
|
||||||
{renderField("eye_color")}
|
|
||||||
{renderField("height_cm", {
|
|
||||||
type: "number",
|
|
||||||
})}
|
|
||||||
{renderField("weight", {
|
|
||||||
type: "number",
|
|
||||||
messageID: "weight_kg",
|
|
||||||
})}
|
|
||||||
{renderField("penis_length", {
|
|
||||||
type: "number",
|
|
||||||
messageID: "penis_length_cm",
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Form.Group as={Row}>
|
{renderInputField("tattoos", "textarea")}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
{renderInputField("piercings", "textarea")}
|
||||||
<FormattedMessage id="circumcised" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs="auto">
|
|
||||||
<Form.Control
|
|
||||||
as="select"
|
|
||||||
className="input-control"
|
|
||||||
{...formik.getFieldProps("circumcised")}
|
|
||||||
>
|
|
||||||
<option value="" key=""></option>
|
|
||||||
{Array.from(stringCircumMap.entries()).map(([name, value]) => (
|
|
||||||
<option value={value} key={value}>
|
|
||||||
{name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderField("measurements")}
|
{renderInputField("career_length")}
|
||||||
{renderField("fake_tits")}
|
|
||||||
|
|
||||||
<Form.Group controlId="tattoos" as={Row}>
|
{renderUrlField()}
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="tattoos" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "tattoos" })}
|
|
||||||
{...formik.getFieldProps("tattoos")}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="piercings" as={Row}>
|
{renderInputField("twitter")}
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
{renderInputField("instagram")}
|
||||||
<FormattedMessage id="piercings" />
|
{renderInputField("details", "textarea")}
|
||||||
</Form.Label>
|
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "piercings" })}
|
|
||||||
{...formik.getFieldProps("piercings")}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderField("career_length")}
|
|
||||||
|
|
||||||
<Form.Group controlId="url" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="url" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<URLField
|
|
||||||
{...formik.getFieldProps("url")}
|
|
||||||
onScrapeClick={onScrapePerformerURL}
|
|
||||||
urlScrapable={urlScrapable}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderField("twitter")}
|
|
||||||
{renderField("instagram")}
|
|
||||||
<Form.Group controlId="details" as={Row}>
|
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="details" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "details" })}
|
|
||||||
{...formik.getFieldProps("details")}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
{renderTagsField()}
|
{renderTagsField()}
|
||||||
|
|
||||||
{renderStashIDs()}
|
{renderStashIDsField("stash_ids", "performers")}
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group controlId="ignore-auto-tag" as={Row}>
|
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="ignore_auto_tag" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Check
|
|
||||||
{...formik.getFieldProps({
|
|
||||||
name: "ignore_auto_tag",
|
|
||||||
type: "checkbox",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderButtons("mt-3")}
|
{renderButtons("mt-3")}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -212,7 +212,11 @@ export const PerformerSelect: React.FC<
|
|||||||
props.noSelectionString ??
|
props.noSelectionString ??
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{ id: "actions.select_entity" },
|
{ id: "actions.select_entity" },
|
||||||
{ entityType: intl.formatMessage({ id: "performer" }) }
|
{
|
||||||
|
entityType: intl.formatMessage({
|
||||||
|
id: props.isMulti ? "performers" : "performer",
|
||||||
|
}),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { StudioSelect } from "../Shared/Select";
|
|||||||
import { ModalComponent } from "../Shared/Modal";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { MultiSet } from "../Shared/MultiSet";
|
import { MultiSet } from "../Shared/MultiSet";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputIDs,
|
getAggregateInputIDs,
|
||||||
@@ -272,7 +272,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={rating100}
|
value={rating100}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setRating(value ?? undefined)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -28,34 +28,30 @@ import {
|
|||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { ImageInput } from "src/components/Shared/ImageInput";
|
import { ImageInput } from "src/components/Shared/ImageInput";
|
||||||
import { URLListInput } from "src/components/Shared/URLField";
|
|
||||||
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 FormUtils from "src/utils/form";
|
|
||||||
import { getStashIDs } from "src/utils/stashIds";
|
import { 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 { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||||
import { SceneMovieTable } from "./SceneMovieTable";
|
import { SceneMovieTable } from "./SceneMovieTable";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { faSearch, faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import {
|
|
||||||
faSearch,
|
|
||||||
faSyncAlt,
|
|
||||||
faTrashAlt,
|
|
||||||
} 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 { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { lazyComponent } from "src/utils/lazyComponent";
|
import { lazyComponent } from "src/utils/lazyComponent";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
import {
|
||||||
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
|
yupDateString,
|
||||||
|
yupFormikValidate,
|
||||||
|
yupUniqueStringList,
|
||||||
|
} from "src/utils/yup";
|
||||||
import {
|
import {
|
||||||
Performer,
|
Performer,
|
||||||
PerformerSelect,
|
PerformerSelect,
|
||||||
} from "src/components/Performers/PerformerSelect";
|
} from "src/components/Performers/PerformerSelect";
|
||||||
import { StashIDPill } from "src/components/Shared/StashID";
|
import { formikUtils } from "src/utils/form";
|
||||||
|
|
||||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
@@ -119,7 +115,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
urls: yupUniqueStringList("urls"),
|
urls: yupUniqueStringList("urls"),
|
||||||
date: yupDateString(intl),
|
date: yupDateString(intl),
|
||||||
director: yup.string().ensure(),
|
director: yup.string().ensure(),
|
||||||
rating100: yup.number().nullable().defined(),
|
rating100: yup.number().integer().nullable().defined(),
|
||||||
gallery_ids: yup.array(yup.string().required()).defined(),
|
gallery_ids: yup.array(yup.string().required()).defined(),
|
||||||
studio_id: yup.string().required().nullable(),
|
studio_id: yup.string().required().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).defined(),
|
performer_ids: yup.array(yup.string().required()).defined(),
|
||||||
@@ -127,7 +123,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
movie_id: yup.string().required(),
|
movie_id: yup.string().required(),
|
||||||
scene_index: yup.number().nullable().defined(),
|
scene_index: yup.number().integer().nullable().defined(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.defined(),
|
.defined(),
|
||||||
@@ -164,8 +160,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validate: yupFormikValidate(schema),
|
||||||
onSubmit: (values) => onSave(values),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const coverImagePreview = useMemo(() => {
|
const coverImagePreview = useMemo(() => {
|
||||||
@@ -275,16 +271,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
|
||||||
formik.setFieldValue(
|
|
||||||
"stash_ids",
|
|
||||||
formik.values.stash_ids.filter(
|
|
||||||
(s) =>
|
|
||||||
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderTableMovies() {
|
function renderTableMovies() {
|
||||||
return (
|
return (
|
||||||
<SceneMovieTable
|
<SceneMovieTable
|
||||||
@@ -656,32 +642,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
|
||||||
return (
|
|
||||||
<Form.Group controlId={field} as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title,
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
placeholder={placeholder ?? title}
|
|
||||||
{...formik.getFieldProps(field)}
|
|
||||||
isInvalid={!!formik.getFieldMeta(field).error}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.getFieldMeta(field).error}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = useMemo(() => {
|
const image = useMemo(() => {
|
||||||
if (encodingImage) {
|
if (encodingImage) {
|
||||||
return (
|
return (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -701,13 +666,124 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
const urlsErrors = Array.isArray(formik.errors.urls)
|
const splitProps = {
|
||||||
? formik.errors.urls[0]
|
labelProps: {
|
||||||
: formik.errors.urls;
|
column: true,
|
||||||
const urlsErrorMsg = urlsErrors
|
sm: 3,
|
||||||
? intl.formatMessage({ id: "validation.urls_must_be_unique" })
|
},
|
||||||
: undefined;
|
fieldProps: {
|
||||||
const urlsErrorIdx = urlsErrors?.split(" ").map((e) => parseInt(e));
|
sm: 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const fullWidthProps = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
renderField,
|
||||||
|
renderInputField,
|
||||||
|
renderDateField,
|
||||||
|
renderRatingField,
|
||||||
|
renderURLListField,
|
||||||
|
renderStashIDsField,
|
||||||
|
} = formikUtils(intl, formik, splitProps);
|
||||||
|
|
||||||
|
function renderGalleriesField() {
|
||||||
|
const title = intl.formatMessage({ id: "galleries" });
|
||||||
|
const control = (
|
||||||
|
<GallerySelect
|
||||||
|
selected={galleries}
|
||||||
|
onSelect={(items) => onSetGalleries(items)}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("gallery_ids", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStudioField() {
|
||||||
|
const title = intl.formatMessage({ id: "studio" });
|
||||||
|
const control = (
|
||||||
|
<StudioSelect
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"studio_id",
|
||||||
|
items.length > 0 ? items[0]?.id : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("studio_id", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPerformersField() {
|
||||||
|
const title = intl.formatMessage({ id: "performers" });
|
||||||
|
const control = (
|
||||||
|
<PerformerSelect isMulti onSelect={onSetPerformers} values={performers} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("performer_ids", title, control, fullWidthProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMoviesField() {
|
||||||
|
const title = intl.formatMessage({ id: "movies" });
|
||||||
|
const control = (
|
||||||
|
<>
|
||||||
|
<MovieSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) => setMovieIds(items.map((item) => item.id))}
|
||||||
|
ids={formik.values.movies.map((m) => m.movie_id)}
|
||||||
|
/>
|
||||||
|
{renderTableMovies()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("movies", title, control, fullWidthProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagsField() {
|
||||||
|
const title = intl.formatMessage({ id: "tags" });
|
||||||
|
const control = (
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"tag_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.tag_ids}
|
||||||
|
hoverPlacement="right"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("tag_ids", title, control, fullWidthProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetailsField() {
|
||||||
|
const props = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderInputField("details", "textarea", "details", props);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="scene-edit-details">
|
<div id="scene-edit-details">
|
||||||
@@ -719,7 +795,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
{renderScrapeQueryModal()}
|
{renderScrapeQueryModal()}
|
||||||
{maybeRenderScrapeDialog()}
|
{maybeRenderScrapeDialog()}
|
||||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||||
<div className="form-container edit-buttons-container row 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">
|
||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
@@ -742,210 +818,46 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isNew && (
|
{!isNew && (
|
||||||
<div className="ml-auto pr-3 text-right d-flex">
|
<div className="ml-auto text-right d-flex">
|
||||||
<ButtonGroup className="scraper-group">
|
<ButtonGroup className="scraper-group">
|
||||||
{renderScraperMenu()}
|
{renderScraperMenu()}
|
||||||
{renderScrapeQueryMenu()}
|
{renderScrapeQueryMenu()}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Row>
|
||||||
<div className="form-container row px-3">
|
<Row className="form-container px-3">
|
||||||
<div className="col-12 col-lg-7 col-xl-12">
|
<Col lg={7} xl={12}>
|
||||||
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
{renderInputField("title")}
|
||||||
{renderTextField("code", intl.formatMessage({ id: "scene_code" }))}
|
{renderInputField("code", "text", "scene_code")}
|
||||||
<Form.Group controlId="urls" as={Row}>
|
|
||||||
<Col xs={3} className="pr-0 url-label">
|
{renderURLListField(
|
||||||
<Form.Label className="col-form-label">
|
"urls",
|
||||||
<FormattedMessage id="urls" />
|
"validation.urls_must_be_unique",
|
||||||
</Form.Label>
|
onScrapeSceneURL,
|
||||||
</Col>
|
urlScrapable
|
||||||
<Col xs={9}>
|
|
||||||
<URLListInput
|
|
||||||
value={formik.values.urls ?? []}
|
|
||||||
setValue={(value) => formik.setFieldValue("urls", value)}
|
|
||||||
errors={urlsErrorMsg}
|
|
||||||
errorIdx={urlsErrorIdx}
|
|
||||||
onScrapeClick={(url) => onScrapeSceneURL(url)}
|
|
||||||
urlScrapable={urlScrapable}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="date" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "date" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<DateInput
|
|
||||||
value={formik.values.date}
|
|
||||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
|
||||||
error={formik.errors.date}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
{renderTextField(
|
|
||||||
"director",
|
|
||||||
intl.formatMessage({ id: "director" })
|
|
||||||
)}
|
)}
|
||||||
<Form.Group controlId="rating" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
{renderDateField("date")}
|
||||||
title: intl.formatMessage({ id: "rating" }),
|
{renderInputField("director")}
|
||||||
})}
|
{renderRatingField("rating100", "rating")}
|
||||||
<Col xs={9}>
|
|
||||||
<RatingSystem
|
{renderGalleriesField()}
|
||||||
value={formik.values.rating100 ?? undefined}
|
{renderStudioField()}
|
||||||
onSetRating={(value) =>
|
{renderPerformersField()}
|
||||||
formik.setFieldValue("rating100", value ?? null)
|
{renderMoviesField()}
|
||||||
}
|
{renderTagsField()}
|
||||||
/>
|
|
||||||
</Col>
|
{renderStashIDsField(
|
||||||
</Form.Group>
|
"stash_ids",
|
||||||
<Form.Group controlId="galleries" as={Row}>
|
"scenes",
|
||||||
{FormUtils.renderLabel({
|
"stash_ids",
|
||||||
title: intl.formatMessage({ id: "galleries" }),
|
fullWidthProps
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9}>
|
|
||||||
<GallerySelect
|
|
||||||
selected={galleries}
|
|
||||||
onSelect={(items) => onSetGalleries(items)}
|
|
||||||
isMulti
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9}>
|
|
||||||
<StudioSelect
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"studio_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="performers" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "performers" }),
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<PerformerSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={onSetPerformers}
|
|
||||||
values={performers}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="moviesScenes" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: `${intl.formatMessage({
|
|
||||||
id: "movies",
|
|
||||||
})}/${intl.formatMessage({ id: "scenes" })}`,
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<MovieSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) =>
|
|
||||||
setMovieIds(items.map((item) => item.id))
|
|
||||||
}
|
|
||||||
ids={formik.values.movies.map((m) => m.movie_id)}
|
|
||||||
/>
|
|
||||||
{renderTableMovies()}
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="tags" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "tags" }),
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<TagSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"tag_ids",
|
|
||||||
items.map((item) => item.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.tag_ids}
|
|
||||||
hoverPlacement="right"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
{formik.values.stash_ids.length ? (
|
|
||||||
<Form.Group controlId="stashIDs">
|
|
||||||
<Form.Label>
|
|
||||||
<FormattedMessage id="stash_ids" />
|
|
||||||
</Form.Label>
|
|
||||||
<ul className="pl-0">
|
|
||||||
{formik.values.stash_ids.map((stashID) => {
|
|
||||||
return (
|
|
||||||
<li key={stashID.stash_id} className="row no-gutters">
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
className="mr-2 py-0"
|
|
||||||
title={intl.formatMessage(
|
|
||||||
{ id: "actions.delete_entity" },
|
|
||||||
{
|
|
||||||
entityType: intl.formatMessage({
|
|
||||||
id: "stash_id",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
onClick={() => removeStashID(stashID)}
|
</Col>
|
||||||
>
|
<Col lg={5} xl={12}>
|
||||||
<Icon icon={faTrashAlt} />
|
{renderDetailsField()}
|
||||||
</Button>
|
<Form.Group controlId="cover_image">
|
||||||
<StashIDPill stashID={stashID} linkType="scenes" />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</Form.Group>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-lg-5 col-xl-12">
|
|
||||||
<Form.Group controlId="details">
|
|
||||||
<Form.Label>
|
|
||||||
<FormattedMessage id="details" />
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="scene-description text-input"
|
|
||||||
onChange={(e) =>
|
|
||||||
formik.setFieldValue("details", e.currentTarget.value)
|
|
||||||
}
|
|
||||||
value={formik.values.details ?? ""}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<div>
|
|
||||||
<Form.Group controlId="cover">
|
|
||||||
<Form.Label>
|
<Form.Label>
|
||||||
<FormattedMessage id="cover_image" />
|
<FormattedMessage id="cover_image" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
@@ -956,9 +868,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
onImageURL={onImageLoad}
|
onImageURL={onImageLoad}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</div>
|
</Col>
|
||||||
</div>
|
</Row>
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -17,6 +17,9 @@ import {
|
|||||||
} from "src/components/Shared/Select";
|
} from "src/components/Shared/Select";
|
||||||
import { getPlayerPosition } from "src/components/ScenePlayer/util";
|
import { getPlayerPosition } from "src/components/ScenePlayer/util";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
import isEqual from "lodash-es/isEqual";
|
||||||
|
import { formikUtils } from "src/utils/form";
|
||||||
|
import { yupFormikValidate } from "src/utils/yup";
|
||||||
|
|
||||||
interface ISceneMarkerForm {
|
interface ISceneMarkerForm {
|
||||||
sceneID: string;
|
sceneID: string;
|
||||||
@@ -29,6 +32,8 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||||||
marker,
|
marker,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const [sceneMarkerCreate] = useSceneMarkerCreate();
|
const [sceneMarkerCreate] = useSceneMarkerCreate();
|
||||||
const [sceneMarkerUpdate] = useSceneMarkerUpdate();
|
const [sceneMarkerUpdate] = useSceneMarkerUpdate();
|
||||||
const [sceneMarkerDestroy] = useSceneMarkerDestroy();
|
const [sceneMarkerDestroy] = useSceneMarkerDestroy();
|
||||||
@@ -38,7 +43,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().ensure(),
|
title: yup.string().ensure(),
|
||||||
seconds: yup.number().required(),
|
seconds: yup.number().min(0).required(),
|
||||||
primary_tag_id: yup.string().required(),
|
primary_tag_id: yup.string().required(),
|
||||||
tag_ids: yup.array(yup.string().required()).defined(),
|
tag_ids: yup.array(yup.string().required()).defined(),
|
||||||
});
|
});
|
||||||
@@ -58,9 +63,9 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||||||
|
|
||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema: schema,
|
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
onSubmit: (values) => onSave(values),
|
validate: yupFormikValidate(schema),
|
||||||
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSave(input: InputValues) {
|
async function onSave(input: InputValues) {
|
||||||
@@ -105,31 +110,49 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||||||
await formik.setFieldTouched("primary_tag_id", true);
|
await formik.setFieldTouched("primary_tag_id", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryTagId = formik.values.primary_tag_id;
|
const splitProps = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const fullWidthProps = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { renderField } = formikUtils(intl, formik, splitProps);
|
||||||
|
|
||||||
return (
|
function renderTitleField() {
|
||||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
const title = intl.formatMessage({ id: "title" });
|
||||||
<div>
|
const control = (
|
||||||
<Form.Group className="row">
|
|
||||||
<Form.Label className="col-sm-3 col-md-2 col-xl-12 col-form-label">
|
|
||||||
Marker Title
|
|
||||||
</Form.Label>
|
|
||||||
<div className="col-sm-9 col-md-10 col-xl-12">
|
|
||||||
<MarkerTitleSuggest
|
<MarkerTitleSuggest
|
||||||
initialMarkerTitle={formik.values.title}
|
initialMarkerTitle={formik.values.title}
|
||||||
onChange={(query: string) => formik.setFieldValue("title", query)}
|
onChange={(v) => formik.setFieldValue("title", v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
);
|
||||||
</Form.Group>
|
|
||||||
<Form.Group className="row">
|
return renderField("title", title, control);
|
||||||
<Form.Label className="col-sm-3 col-md-2 col-xl-12 col-form-label">
|
}
|
||||||
Primary Tag
|
|
||||||
</Form.Label>
|
function renderPrimaryTagField() {
|
||||||
<div className="col-sm-4 col-md-6 col-xl-12 mb-3 mb-sm-0 mb-xl-3">
|
const primaryTagId = formik.values.primary_tag_id;
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ id: "primary_tag" });
|
||||||
|
const control = (
|
||||||
|
<>
|
||||||
<TagSelect
|
<TagSelect
|
||||||
onSelect={onSetPrimaryTagID}
|
onSelect={onSetPrimaryTagID}
|
||||||
ids={primaryTagId ? [primaryTagId] : []}
|
ids={primaryTagId ? [primaryTagId] : []}
|
||||||
noSelectionString="Select/create tag..."
|
|
||||||
hoverPlacement="right"
|
hoverPlacement="right"
|
||||||
/>
|
/>
|
||||||
{formik.touched.primary_tag_id && (
|
{formik.touched.primary_tag_id && (
|
||||||
@@ -137,52 +160,62 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||||||
{formik.errors.primary_tag_id}
|
{formik.errors.primary_tag_id}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
<div className="col-sm-5 col-md-4 col-xl-12">
|
);
|
||||||
<div className="row">
|
|
||||||
<Form.Label className="col-sm-4 col-md-4 col-xl-12 col-form-label text-sm-right text-xl-left">
|
return renderField("primary_tag_id", title, control);
|
||||||
Time
|
|
||||||
</Form.Label>
|
|
||||||
<div className="col-sm-8 col-xl-12">
|
|
||||||
<DurationInput
|
|
||||||
value={formik.values.seconds ?? 0}
|
|
||||||
setValue={(v) => formik.setFieldValue("seconds", v ?? null)}
|
|
||||||
onReset={() =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"seconds",
|
|
||||||
Math.round(getPlayerPosition() ?? 0)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTimeField() {
|
||||||
|
const { error } = formik.getFieldMeta("seconds");
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ id: "time" });
|
||||||
|
const control = (
|
||||||
|
<DurationInput
|
||||||
|
value={formik.values.seconds}
|
||||||
|
setValue={(v) => formik.setFieldValue("seconds", v)}
|
||||||
|
onReset={() =>
|
||||||
|
formik.setFieldValue("seconds", Math.round(getPlayerPosition() ?? 0))
|
||||||
|
}
|
||||||
|
error={error}
|
||||||
/>
|
/>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
</div>
|
return renderField("seconds", title, control);
|
||||||
</Form.Group>
|
}
|
||||||
<Form.Group className="row">
|
|
||||||
<Form.Label className="col-sm-3 col-md-2 col-xl-12 col-form-label">
|
function renderTagsField() {
|
||||||
Tags
|
const title = intl.formatMessage({ id: "tags" });
|
||||||
</Form.Label>
|
const control = (
|
||||||
<div className="col-sm-9 col-md-10 col-xl-12">
|
|
||||||
<TagSelect
|
<TagSelect
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(tags) =>
|
onSelect={(items) =>
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
"tag_ids",
|
"tag_ids",
|
||||||
tags.map((tag) => tag.id)
|
items.map((item) => item.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ids={formik.values.tag_ids}
|
ids={formik.values.tag_ids}
|
||||||
noSelectionString="Select/create tags..."
|
|
||||||
hoverPlacement="right"
|
hoverPlacement="right"
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("tag_ids", title, control, fullWidthProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||||
|
<div className="form-container px-3">
|
||||||
|
{renderTitleField()}
|
||||||
|
{renderPrimaryTagField()}
|
||||||
|
{renderTimeField()}
|
||||||
|
{renderTagsField()}
|
||||||
</div>
|
</div>
|
||||||
</Form.Group>
|
<div className="buttons-container px-3">
|
||||||
</div>
|
<div className="d-flex">
|
||||||
<div className="buttons-container row">
|
|
||||||
<div className="col d-flex">
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!isNew && !formik.dirty}
|
disabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
onClick={() => formik.submitForm()}
|
onClick={() => formik.submitForm()}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="actions.save" />
|
<FormattedMessage id="actions.save" />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { Icon } from "../Shared/Icon";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||||
import { StringListSelect, GallerySelect, SceneSelect } from "../Shared/Select";
|
import { StringListSelect, GallerySelect, SceneSelect } from "../Shared/Select";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { mutateSceneMerge, queryFindScenesByID } from "src/core/StashService";
|
import { mutateSceneMerge, queryFindScenesByID } from "src/core/StashService";
|
||||||
|
|||||||
@@ -67,10 +67,6 @@
|
|||||||
.tab-content {
|
.tab-content {
|
||||||
min-height: 15rem;
|
min-height: 15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-description {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.scene-description {
|
textarea.scene-description {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
StringSetting,
|
StringSetting,
|
||||||
} from "../Inputs";
|
} from "../Inputs";
|
||||||
import { useSettings } from "../context";
|
import { useSettings } from "../context";
|
||||||
import DurationUtils from "src/utils/duration";
|
import TextUtils from "src/utils/text";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
imageLightboxDisplayModeIntlMap,
|
imageLightboxDisplayModeIntlMap,
|
||||||
@@ -361,7 +361,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderValue={(v) => {
|
renderValue={(v) => {
|
||||||
return <span>{DurationUtils.secondsToString(v ?? 0)}</span>;
|
return <span>{TextUtils.secondsToTimestamp(v ?? 0)}</span>;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { faCalendar } from "@fortawesome/free-regular-svg-icons";
|
import { faCalendar } from "@fortawesome/free-regular-svg-icons";
|
||||||
import React, { useMemo } from "react";
|
import React, { forwardRef, useMemo } from "react";
|
||||||
import { Button, InputGroup, Form } from "react-bootstrap";
|
import { Button, InputGroup, Form } from "react-bootstrap";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
@@ -10,13 +10,24 @@ import { useIntl } from "react-intl";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
value: string | undefined;
|
value: string;
|
||||||
isTime?: boolean;
|
isTime?: boolean;
|
||||||
onValueChange(value: string): void;
|
onValueChange(value: string): void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ShowPickerButton = forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
{
|
||||||
|
onClick: (event: React.MouseEvent) => void;
|
||||||
|
}
|
||||||
|
>(({ onClick }, ref) => (
|
||||||
|
<Button variant="secondary" onClick={onClick} ref={ref}>
|
||||||
|
<Icon icon={faCalendar} />
|
||||||
|
</Button>
|
||||||
|
));
|
||||||
|
|
||||||
export const DateInput: React.FC<IProps> = (props: IProps) => {
|
export const DateInput: React.FC<IProps> = (props: IProps) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -26,28 +37,14 @@ export const DateInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
: TextUtils.stringToFuzzyDate;
|
: TextUtils.stringToFuzzyDate;
|
||||||
if (props.value) {
|
if (props.value) {
|
||||||
const ret = toDate(props.value);
|
const ret = toDate(props.value);
|
||||||
if (!ret || isNaN(ret.getTime())) {
|
if (ret && !Number.isNaN(ret.getTime())) {
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [props.value, props.isTime]);
|
}, [props.value, props.isTime]);
|
||||||
|
|
||||||
function maybeRenderButton() {
|
function maybeRenderButton() {
|
||||||
if (!props.disabled) {
|
if (!props.disabled) {
|
||||||
const ShowPickerButton = ({
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
onClick: (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
) => void;
|
|
||||||
}) => (
|
|
||||||
<Button variant="secondary" onClick={onClick}>
|
|
||||||
<Icon icon={faCalendar} />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const dateToString = props.isTime
|
const dateToString = props.isTime
|
||||||
? TextUtils.dateTimeToString
|
? TextUtils.dateTimeToString
|
||||||
: TextUtils.dateToString;
|
: TextUtils.dateToString;
|
||||||
@@ -83,9 +80,7 @@ export const DateInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
className="date-input text-input"
|
className="date-input text-input"
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e) => props.onValueChange(e.currentTarget.value)}
|
||||||
props.onValueChange(e.currentTarget.value)
|
|
||||||
}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
!props.disabled
|
!props.disabled
|
||||||
? props.placeholder
|
? props.placeholder
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ import {
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap";
|
import { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
import DurationUtils from "src/utils/duration";
|
import TextUtils from "src/utils/text";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
value: number | undefined;
|
value: number | null | undefined;
|
||||||
setValue(value: number | undefined): void;
|
setValue(value: number | null): void;
|
||||||
onReset?(): void;
|
onReset?(): void;
|
||||||
className?: string;
|
className?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
error?: string;
|
||||||
|
allowNegative?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DurationInput: React.FC<IProps> = ({
|
export const DurationInput: React.FC<IProps> = ({
|
||||||
@@ -24,6 +26,8 @@ export const DurationInput: React.FC<IProps> = ({
|
|||||||
onReset,
|
onReset,
|
||||||
className,
|
className,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
error,
|
||||||
|
allowNegative = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [tmpValue, setTmpValue] = useState<string>();
|
const [tmpValue, setTmpValue] = useState<string>();
|
||||||
|
|
||||||
@@ -33,19 +37,30 @@ export const DurationInput: React.FC<IProps> = ({
|
|||||||
|
|
||||||
function onBlur() {
|
function onBlur() {
|
||||||
if (tmpValue !== undefined) {
|
if (tmpValue !== undefined) {
|
||||||
setValue(DurationUtils.stringToSeconds(tmpValue));
|
updateValue(TextUtils.timestampToSeconds(tmpValue));
|
||||||
setTmpValue(undefined);
|
setTmpValue(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateValue(v: number | null) {
|
||||||
|
if (v !== null && !allowNegative && v < 0) {
|
||||||
|
v = null;
|
||||||
|
}
|
||||||
|
setValue(v);
|
||||||
|
}
|
||||||
|
|
||||||
function increment() {
|
function increment() {
|
||||||
setTmpValue(undefined);
|
setTmpValue(undefined);
|
||||||
setValue((value ?? 0) + 1);
|
updateValue((value ?? 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrement() {
|
function decrement() {
|
||||||
setTmpValue(undefined);
|
setTmpValue(undefined);
|
||||||
setValue((value ?? 0) - 1);
|
if (allowNegative) {
|
||||||
|
updateValue((value ?? 0) - 1);
|
||||||
|
} else {
|
||||||
|
updateValue(value ? value - 1 : 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderButtons() {
|
function renderButtons() {
|
||||||
@@ -84,8 +99,14 @@ export const DurationInput: React.FC<IProps> = ({
|
|||||||
let inputValue = "";
|
let inputValue = "";
|
||||||
if (tmpValue !== undefined) {
|
if (tmpValue !== undefined) {
|
||||||
inputValue = tmpValue;
|
inputValue = tmpValue;
|
||||||
} else if (value !== undefined) {
|
} else if (value !== null && value !== undefined) {
|
||||||
inputValue = DurationUtils.secondsToString(value);
|
inputValue = TextUtils.secondsToTimestamp(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholder) {
|
||||||
|
placeholder = `${placeholder} (hh:mm:ss)`;
|
||||||
|
} else {
|
||||||
|
placeholder = "hh:mm:ss";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -97,12 +118,13 @@ export const DurationInput: React.FC<IProps> = ({
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
placeholder={placeholder ? `${placeholder} (hh:mm:ss)` : "hh:mm:ss"}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
{maybeRenderReset()}
|
{maybeRenderReset()}
|
||||||
{renderButtons()}
|
{renderButtons()}
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
|
<Form.Control.Feedback type="invalid">{error}</Form.Control.Feedback>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
export interface IRatingNumberProps {
|
export interface IRatingNumberProps {
|
||||||
value?: number;
|
value: number | null;
|
||||||
onSetRating?: (value?: number) => void;
|
onSetRating?: (value: number | null) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export const RatingNumber: React.FC<IRatingNumberProps> = (
|
|||||||
if (!useValidation.current) {
|
if (!useValidation.current) {
|
||||||
e.target.value = Number(val).toFixed(1);
|
e.target.value = Number(val).toFixed(1);
|
||||||
const tempVal = Number(val) * 10;
|
const tempVal = Number(val) * 10;
|
||||||
props.onSetRating(tempVal != 0 ? tempVal : undefined);
|
props.onSetRating(tempVal || null);
|
||||||
useValidation.current = true;
|
useValidation.current = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export const RatingNumber: React.FC<IRatingNumberProps> = (
|
|||||||
}
|
}
|
||||||
e.target.value = Number(value).toFixed(1);
|
e.target.value = Number(value).toFixed(1);
|
||||||
let tempVal = Number(value) * 10;
|
let tempVal = Number(value) * 10;
|
||||||
props.onSetRating(tempVal != 0 ? tempVal : undefined);
|
props.onSetRating(tempVal || null);
|
||||||
|
|
||||||
let cursorPosition = 0;
|
let cursorPosition = 0;
|
||||||
if (match[2] && !match[4]) {
|
if (match[2] && !match[4]) {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
export interface IRatingStarsProps {
|
export interface IRatingStarsProps {
|
||||||
value?: number;
|
value: number | null;
|
||||||
onSetRating?: (value?: number) => void;
|
onSetRating?: (value: number | null) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
precision: RatingStarPrecision;
|
precision: RatingStarPrecision;
|
||||||
valueRequired?: boolean;
|
valueRequired?: boolean;
|
||||||
@@ -87,7 +87,7 @@ export const RatingStars: React.FC<IRatingStarsProps> = (
|
|||||||
setHoverRating(undefined);
|
setHoverRating(undefined);
|
||||||
|
|
||||||
if (!newRating) {
|
if (!newRating) {
|
||||||
props.onSetRating(undefined);
|
props.onSetRating(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { RatingNumber } from "./RatingNumber";
|
|||||||
import { RatingStars } from "./RatingStars";
|
import { RatingStars } from "./RatingStars";
|
||||||
|
|
||||||
export interface IRatingSystemProps {
|
export interface IRatingSystemProps {
|
||||||
value?: number;
|
value: number | null | undefined;
|
||||||
onSetRating?: (value?: number) => void;
|
onSetRating?: (value: number | null) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
valueRequired?: boolean;
|
valueRequired?: boolean;
|
||||||
}
|
}
|
||||||
@@ -24,10 +24,10 @@ export const RatingSystem: React.FC<IRatingSystemProps> = (
|
|||||||
(config?.ui as IUIConfig)?.ratingSystemOptions ??
|
(config?.ui as IUIConfig)?.ratingSystemOptions ??
|
||||||
defaultRatingSystemOptions;
|
defaultRatingSystemOptions;
|
||||||
|
|
||||||
function getRatingStars() {
|
if (ratingSystemOptions.type === RatingSystemType.Stars) {
|
||||||
return (
|
return (
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={props.value}
|
value={props.value ?? null}
|
||||||
onSetRating={props.onSetRating}
|
onSetRating={props.onSetRating}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
precision={
|
precision={
|
||||||
@@ -36,14 +36,10 @@ export const RatingSystem: React.FC<IRatingSystemProps> = (
|
|||||||
valueRequired={props.valueRequired}
|
valueRequired={props.valueRequired}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (ratingSystemOptions.type === RatingSystemType.Stars) {
|
|
||||||
return getRatingStars();
|
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<RatingNumber
|
<RatingNumber
|
||||||
value={props.value}
|
value={props.value ?? null}
|
||||||
onSetRating={props.onSetRating}
|
onSetRating={props.onSetRating}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useToast } from "src/hooks/Toast";
|
|||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Col, Form, Row } from "react-bootstrap";
|
import { Col, Form, Row } from "react-bootstrap";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import { mutateSceneAssignFile } from "src/core/StashService";
|
import { mutateSceneAssignFile } from "src/core/StashService";
|
||||||
|
|
||||||
interface IFile {
|
interface IFile {
|
||||||
|
|||||||
@@ -662,7 +662,11 @@ export const StudioSelect: React.FC<
|
|||||||
props.noSelectionString ??
|
props.noSelectionString ??
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{ id: "actions.select_entity" },
|
{ id: "actions.select_entity" },
|
||||||
{ entityType: intl.formatMessage({ id: "studio" }) }
|
{
|
||||||
|
entityType: intl.formatMessage({
|
||||||
|
id: props.isMulti ? "studios" : "studio",
|
||||||
|
}),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
creatable={props.creatable ?? defaultCreatable}
|
creatable={props.creatable ?? defaultCreatable}
|
||||||
@@ -705,7 +709,11 @@ export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
|||||||
props.noSelectionString ??
|
props.noSelectionString ??
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{ id: "actions.select_entity" },
|
{ id: "actions.select_entity" },
|
||||||
{ entityType: intl.formatMessage({ id: "movie" }) }
|
{
|
||||||
|
entityType: intl.formatMessage({
|
||||||
|
id: props.isMulti ? "movies" : "movie",
|
||||||
|
}),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
creatable={props.creatable ?? defaultCreatable}
|
creatable={props.creatable ?? defaultCreatable}
|
||||||
@@ -726,7 +734,7 @@ export const TagSelect: React.FC<
|
|||||||
props.noSelectionString ??
|
props.noSelectionString ??
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{ id: "actions.select_entity" },
|
{ id: "actions.select_entity" },
|
||||||
{ entityType: intl.formatMessage({ id: "tags" }) }
|
{ entityType: intl.formatMessage({ id: props.isMulti ? "tags" : "tag" }) }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { StashId } from "src/core/generated-graphql";
|
|||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { getStashboxBase } from "src/utils/stashbox";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
|
|
||||||
type LinkType = "performers" | "scenes" | "studios";
|
export type LinkType = "performers" | "scenes" | "studios";
|
||||||
|
|
||||||
export const StashIDPill: React.FC<{
|
export const StashIDPill: React.FC<{
|
||||||
stashID: StashId;
|
stashID: StashId;
|
||||||
|
|||||||
@@ -235,7 +235,6 @@ export const TagLink: React.FC<ITagLinkProps> = ({
|
|||||||
return (
|
return (
|
||||||
<CommonLinkComponent link={link} className={className}>
|
<CommonLinkComponent link={link} className={className}>
|
||||||
<TagPopover id={tag.id ?? ""} placement={hoverPlacement}>
|
<TagPopover id={tag.id ?? ""} placement={hoverPlacement}>
|
||||||
<Link to={link}>
|
|
||||||
{title}
|
{title}
|
||||||
{showHierarchyIcon && (
|
{showHierarchyIcon && (
|
||||||
<OverlayTrigger placement="top" overlay={tooltip}>
|
<OverlayTrigger placement="top" overlay={tooltip}>
|
||||||
@@ -245,7 +244,6 @@ export const TagLink: React.FC<ITagLinkProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
)}
|
)}
|
||||||
</Link>
|
|
||||||
</TagPopover>
|
</TagPopover>
|
||||||
</CommonLinkComponent>
|
</CommonLinkComponent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -526,7 +526,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
|||||||
<div className="detail-header-image">
|
<div className="detail-header-image">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
@@ -541,8 +541,8 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
|||||||
</h2>
|
</h2>
|
||||||
{maybeRenderAliases()}
|
{maybeRenderAliases()}
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={studio.rating100 ?? undefined}
|
value={studio.rating100}
|
||||||
onSetRating={(value) => setRating(value ?? null)}
|
onSetRating={(value) => setRating(value)}
|
||||||
/>
|
/>
|
||||||
{maybeRenderDetails()}
|
{maybeRenderDetails()}
|
||||||
{maybeRenderEditPanel()}
|
{maybeRenderEditPanel()}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const StudioCreate: React.FC = () => {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
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";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { StudioSelect } from "src/components/Shared/Select";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { Button, Form, Col, Row } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import { getStashIDs } from "src/utils/stashIds";
|
import { 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 { StringListInput } from "../../Shared/StringListInput";
|
|
||||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
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 { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import { StashIDPill } from "src/components/Shared/StashID";
|
import { formikUtils } from "src/utils/form";
|
||||||
|
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
||||||
|
|
||||||
interface IStudioEditPanel {
|
interface IStudioEditPanel {
|
||||||
studio: Partial<GQL.StudioDataFragment>;
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
@@ -41,11 +39,6 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
|
|
||||||
const isNew = studio.id === undefined;
|
const isNew = studio.id === undefined;
|
||||||
|
|
||||||
const labelXS = 3;
|
|
||||||
const labelXL = 2;
|
|
||||||
const fieldXS = 9;
|
|
||||||
const fieldXL = 7;
|
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -54,26 +47,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
url: yup.string().ensure(),
|
url: yup.string().ensure(),
|
||||||
details: yup.string().ensure(),
|
details: yup.string().ensure(),
|
||||||
parent_id: yup.string().required().nullable(),
|
parent_id: yup.string().required().nullable(),
|
||||||
aliases: yup
|
aliases: yupUniqueAliases("aliases", "name"),
|
||||||
.array(yup.string().required())
|
|
||||||
.defined()
|
|
||||||
.test({
|
|
||||||
name: "unique",
|
|
||||||
test: (value, context) => {
|
|
||||||
const aliases = [context.parent.name, ...value];
|
|
||||||
const dupes = aliases
|
|
||||||
.map((e, i, a) => {
|
|
||||||
if (a.indexOf(e) !== i) {
|
|
||||||
return String(i - 1);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((e) => e !== null) as string[];
|
|
||||||
if (dupes.length === 0) return true;
|
|
||||||
return new yup.ValidationError(dupes.join(" "), value, "aliases");
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
ignore_auto_tag: yup.boolean().defined(),
|
ignore_auto_tag: yup.boolean().defined(),
|
||||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||||
image: yup.string().nullable().optional(),
|
image: yup.string().nullable().optional(),
|
||||||
@@ -95,8 +69,8 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validate: yupFormikValidate(schema),
|
||||||
onSubmit: (values) => onSave(values),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
||||||
@@ -143,60 +117,30 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
const {
|
||||||
|
renderField,
|
||||||
|
renderInputField,
|
||||||
|
renderStringListField,
|
||||||
|
renderStashIDsField,
|
||||||
|
} = formikUtils(intl, formik);
|
||||||
|
|
||||||
|
function renderParentStudioField() {
|
||||||
|
const title = intl.formatMessage({ id: "parent_studio" });
|
||||||
|
const control = (
|
||||||
|
<StudioSelect
|
||||||
|
onSelect={(items) =>
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
"stash_ids",
|
"parent_id",
|
||||||
(formik.values.stash_ids ?? []).filter(
|
items.length > 0 ? items[0]?.id : null
|
||||||
(s) =>
|
|
||||||
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
|
|
||||||
)
|
)
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderStashIDs() {
|
|
||||||
if (!formik.values.stash_ids?.length) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
ids={formik.values.parent_id ? [formik.values.parent_id] : []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return renderField("parent_id", title, control);
|
||||||
<Row>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
{intl.formatMessage({ id: "stash_ids" })}
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<ul className="pl-0">
|
|
||||||
{formik.values.stash_ids.map((stashID) => {
|
|
||||||
return (
|
|
||||||
<li key={stashID.stash_id} className="row no-gutters">
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
className="mr-2 py-0"
|
|
||||||
title={intl.formatMessage(
|
|
||||||
{ id: "actions.delete_entity" },
|
|
||||||
{ entityType: intl.formatMessage({ id: "stash_id" }) }
|
|
||||||
)}
|
|
||||||
onClick={() => removeStashID(stashID)}
|
|
||||||
>
|
|
||||||
<Icon icon={faTrashAlt} />
|
|
||||||
</Button>
|
|
||||||
<StashIDPill stashID={stashID} linkType="studios" />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const aliasErrors = Array.isArray(formik.errors.aliases)
|
|
||||||
? formik.errors.aliases[0]
|
|
||||||
: formik.errors.aliases;
|
|
||||||
const aliasErrorMsg = aliasErrors
|
|
||||||
? intl.formatMessage({ id: "validation.aliases_must_be_unique" })
|
|
||||||
: undefined;
|
|
||||||
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -213,105 +157,15 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
|
<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
|
||||||
<Form.Group controlId="name" as={Row}>
|
{renderInputField("name")}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
{renderStringListField("aliases", "validation.aliases_must_be_unique")}
|
||||||
<FormattedMessage id="name" />
|
{renderInputField("url")}
|
||||||
</Form.Label>
|
{renderInputField("details", "textarea")}
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
{renderParentStudioField()}
|
||||||
<Form.Control
|
{renderStashIDsField("stash_ids", "studios")}
|
||||||
className="text-input"
|
|
||||||
{...formik.getFieldProps("name")}
|
|
||||||
isInvalid={!!formik.errors.name}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.name}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="aliases" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="aliases" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<StringListInput
|
|
||||||
value={formik.values.aliases ?? []}
|
|
||||||
setValue={(value) => formik.setFieldValue("aliases", value)}
|
|
||||||
errors={aliasErrorMsg}
|
|
||||||
errorIdx={aliasErrorIdx}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="url" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="url" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
{...formik.getFieldProps("url")}
|
|
||||||
isInvalid={!!formik.errors.url}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.url}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="details" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="details" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="text-input"
|
|
||||||
{...formik.getFieldProps("details")}
|
|
||||||
isInvalid={!!formik.errors.details}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.details}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="parent_studio" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="parent_studios" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<StudioSelect
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"parent_id",
|
|
||||||
items.length > 0 ? items[0]?.id : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.parent_id ? [formik.values.parent_id] : []}
|
|
||||||
excludeIds={studio.id ? [studio.id] : []}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderStashIDs()}
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||||
<Form.Group controlId="ignore-auto-tag" as={Row}>
|
</Form>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="ignore_auto_tag" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Check
|
|
||||||
{...formik.getFieldProps({
|
|
||||||
name: "ignore_auto_tag",
|
|
||||||
type: "checkbox",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<DetailsEditNavbar
|
<DetailsEditNavbar
|
||||||
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
|
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { SuccessIcon } from "src/components/Shared/SuccessIcon";
|
|||||||
import { TagSelect } from "src/components/Shared/Select";
|
import { TagSelect } from "src/components/Shared/Select";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { OperationButton } from "src/components/Shared/OperationButton";
|
import { OperationButton } from "src/components/Shared/OperationButton";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import { stringToGender } from "src/utils/gender";
|
import { stringToGender } from "src/utils/gender";
|
||||||
import { IScrapedScene, TaggerStateContext } from "../context";
|
import { IScrapedScene, TaggerStateContext } from "../context";
|
||||||
import { OptionalField } from "../IncludeButton";
|
import { OptionalField } from "../IncludeButton";
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
|||||||
<div className="detail-header-image">
|
<div className="detail-header-image">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const TagCreate: React.FC = () => {
|
|||||||
<div className="text-center logo-container">
|
<div className="text-center logo-container">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { TagSelect } from "src/components/Shared/Select";
|
import { TagSelect } from "src/components/Shared/Select";
|
||||||
import { Form, Col, Row } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
|
||||||
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 { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
|
import { formikUtils } from "src/utils/form";
|
||||||
|
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
||||||
|
|
||||||
interface ITagEditPanel {
|
interface ITagEditPanel {
|
||||||
tag: Partial<GQL.TagDataFragment>;
|
tag: Partial<GQL.TagDataFragment>;
|
||||||
@@ -40,33 +41,9 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const labelXS = 3;
|
|
||||||
const labelXL = 2;
|
|
||||||
const fieldXS = 9;
|
|
||||||
const fieldXL = 7;
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
aliases: yup
|
aliases: yupUniqueAliases("aliases", "name"),
|
||||||
.array(yup.string().required())
|
|
||||||
.defined()
|
|
||||||
.test({
|
|
||||||
name: "unique",
|
|
||||||
test: (value, context) => {
|
|
||||||
const aliases = [context.parent.name, ...value];
|
|
||||||
const dupes = aliases
|
|
||||||
.map((e, i, a) => {
|
|
||||||
if (a.indexOf(e) !== i) {
|
|
||||||
return String(i - 1);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((e) => e !== null) as string[];
|
|
||||||
if (dupes.length === 0) return true;
|
|
||||||
return new yup.ValidationError(dupes.join(" "), value, "aliases");
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
description: yup.string().ensure(),
|
description: yup.string().ensure(),
|
||||||
parent_ids: yup.array(yup.string().required()).defined(),
|
parent_ids: yup.array(yup.string().required()).defined(),
|
||||||
child_ids: yup.array(yup.string().required()).defined(),
|
child_ids: yup.array(yup.string().required()).defined(),
|
||||||
@@ -87,9 +64,9 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
|
|
||||||
const formik = useFormik<InputValues>({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema: schema,
|
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
onSubmit: (values) => onSave(values),
|
validate: yupFormikValidate(schema),
|
||||||
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
@@ -134,18 +111,55 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
const aliasErrors = Array.isArray(formik.errors.aliases)
|
const { renderField, renderInputField, renderStringListField } = formikUtils(
|
||||||
? formik.errors.aliases[0]
|
intl,
|
||||||
: formik.errors.aliases;
|
formik
|
||||||
const aliasErrorMsg = aliasErrors
|
);
|
||||||
? intl.formatMessage({ id: "validation.aliases_must_be_unique" })
|
|
||||||
: undefined;
|
function renderParentTagsField() {
|
||||||
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
const title = intl.formatMessage({ id: "parent_tags" });
|
||||||
|
const control = (
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"parent_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.parent_ids}
|
||||||
|
excludeIds={[...(tag?.id ? [tag.id] : []), ...formik.values.child_ids]}
|
||||||
|
creatable={false}
|
||||||
|
hoverPlacement="right"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("parent_ids", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubTagsField() {
|
||||||
|
const title = intl.formatMessage({ id: "sub_tags" });
|
||||||
|
const control = (
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"child_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.child_ids}
|
||||||
|
excludeIds={[...(tag?.id ? [tag.id] : []), ...formik.values.parent_ids]}
|
||||||
|
creatable={false}
|
||||||
|
hoverPlacement="right"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField("child_ids", title, control);
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
const isEditing = true;
|
|
||||||
|
|
||||||
// TODO: CSS class
|
// TODO: CSS class
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -171,121 +185,20 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
|
<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
|
||||||
<Form.Group controlId="name" as={Row}>
|
{renderInputField("name")}
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
{renderStringListField("aliases", "validation.aliases_must_be_unique")}
|
||||||
<FormattedMessage id="name" />
|
{renderInputField("description", "textarea")}
|
||||||
</Form.Label>
|
{renderParentTagsField()}
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
{renderSubTagsField()}
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "name" })}
|
|
||||||
{...formik.getFieldProps("name")}
|
|
||||||
isInvalid={!!formik.errors.name}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formik.errors.name}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="aliases" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="aliases" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<StringListInput
|
|
||||||
value={formik.values.aliases}
|
|
||||||
setValue={(value) => formik.setFieldValue("aliases", value)}
|
|
||||||
errors={aliasErrorMsg}
|
|
||||||
errorIdx={aliasErrorIdx}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="description" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="description" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="text-input"
|
|
||||||
placeholder={intl.formatMessage({ id: "description" })}
|
|
||||||
{...formik.getFieldProps("description")}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="parent_tags" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="parent_tags" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<TagSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"parent_ids",
|
|
||||||
items.map((item) => item.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.parent_ids}
|
|
||||||
excludeIds={[
|
|
||||||
...(tag?.id ? [tag.id] : []),
|
|
||||||
...formik.values.child_ids,
|
|
||||||
]}
|
|
||||||
creatable={false}
|
|
||||||
hoverPlacement="right"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="sub_tags" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="sub_tags" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<TagSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) =>
|
|
||||||
formik.setFieldValue(
|
|
||||||
"child_ids",
|
|
||||||
items.map((item) => item.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ids={formik.values.child_ids}
|
|
||||||
excludeIds={[
|
|
||||||
...(tag?.id ? [tag.id] : []),
|
|
||||||
...formik.values.parent_ids,
|
|
||||||
]}
|
|
||||||
creatable={false}
|
|
||||||
hoverPlacement="right"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||||
<Form.Group controlId="ignore-auto-tag" as={Row}>
|
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
|
||||||
<FormattedMessage id="ignore_auto_tag" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
|
||||||
<Form.Check
|
|
||||||
{...formik.getFieldProps({
|
|
||||||
name: "ignore_auto_tag",
|
|
||||||
type: "checkbox",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<DetailsEditNavbar
|
<DetailsEditNavbar
|
||||||
objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
|
objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
|
||||||
classNames="col-xl-9 mt-3"
|
classNames="col-xl-9 mt-3"
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing={isEditing}
|
isEditing
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
onSave={formik.handleSubmit}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useState } from "react";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { ModalComponent } from "src/components/Shared/Modal";
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
import { TagSelect } from "src/components/Shared/Select";
|
import { TagSelect } from "src/components/Shared/Select";
|
||||||
import FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import { useTagsMerge } from "src/core/StashService";
|
import { useTagsMerge } from "src/core/StashService";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import DurationUtils from "src/utils/duration";
|
import TextUtils from "src/utils/text";
|
||||||
|
|
||||||
export const scrapedMovieToCreateInput = (toCreate: GQL.ScrapedMovie) => {
|
export const scrapedMovieToCreateInput = (toCreate: GQL.ScrapedMovie) => {
|
||||||
const input: GQL.MovieCreateInput = {
|
const input: GQL.MovieCreateInput = {
|
||||||
@@ -11,9 +11,7 @@ export const scrapedMovieToCreateInput = (toCreate: GQL.ScrapedMovie) => {
|
|||||||
synopsis: toCreate.synopsis,
|
synopsis: toCreate.synopsis,
|
||||||
date: toCreate.date,
|
date: toCreate.date,
|
||||||
// #788 - convert duration and rating to the correct type
|
// #788 - convert duration and rating to the correct type
|
||||||
duration: toCreate.duration
|
duration: TextUtils.timestampToSeconds(toCreate.duration),
|
||||||
? DurationUtils.stringToSeconds(toCreate.duration)
|
|
||||||
: undefined,
|
|
||||||
studio_id: toCreate.studio?.stored_id,
|
studio_id: toCreate.studio?.stored_id,
|
||||||
rating100: parseInt(toCreate.rating ?? "0", 10) * 20,
|
rating100: parseInt(toCreate.rating ?? "0", 10) * 20,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -921,10 +921,8 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<RatingSystem
|
<RatingSystem
|
||||||
value={currentImage?.rating100 ?? undefined}
|
value={currentImage?.rating100}
|
||||||
onSetRating={(v) => {
|
onSetRating={(v) => setRating(v)}
|
||||||
setRating(v ?? null);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"delete_file": "Delete file",
|
"delete_file": "Delete file",
|
||||||
"delete_file_and_funscript": "Delete file (and funscript)",
|
"delete_file_and_funscript": "Delete file (and funscript)",
|
||||||
"delete_generated_supporting_files": "Delete generated supporting files",
|
"delete_generated_supporting_files": "Delete generated supporting files",
|
||||||
"delete_stashid": "Delete StashID",
|
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
"disallow": "Disallow",
|
"disallow": "Disallow",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
@@ -42,7 +41,7 @@
|
|||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"edit_entity": "Edit {entityType}",
|
"edit_entity": "Edit {entityType}",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"encoding_image": "Encoding image",
|
"encoding_image": "Encoding image…",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"export_all": "Export all…",
|
"export_all": "Export all…",
|
||||||
"find": "Find",
|
"find": "Find",
|
||||||
@@ -1130,6 +1129,7 @@
|
|||||||
"play_count": "Play Count",
|
"play_count": "Play Count",
|
||||||
"play_duration": "Play Duration",
|
"play_duration": "Play Duration",
|
||||||
"primary_file": "Primary file",
|
"primary_file": "Primary file",
|
||||||
|
"primary_tag": "Primary Tag",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"random": "Random",
|
"random": "Random",
|
||||||
"rating": "Rating",
|
"rating": "Rating",
|
||||||
@@ -1325,6 +1325,7 @@
|
|||||||
"tag_sub_tag_tooltip": "Has sub-tags",
|
"tag_sub_tag_tooltip": "Has sub-tags",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"tattoos": "Tattoos",
|
"tattoos": "Tattoos",
|
||||||
|
"time": "Time",
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"toast": {
|
"toast": {
|
||||||
"added_entity": "Added {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
"added_entity": "Added {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
TimestampCriterionInput,
|
TimestampCriterionInput,
|
||||||
ConfigDataFragment,
|
ConfigDataFragment,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import DurationUtils from "src/utils/duration";
|
import TextUtils from "src/utils/text";
|
||||||
import {
|
import {
|
||||||
CriterionType,
|
CriterionType,
|
||||||
IHierarchicalLabelValue,
|
IHierarchicalLabelValue,
|
||||||
@@ -770,8 +770,8 @@ export class DurationCriterion extends Criterion<INumberValue> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getLabelValue(_intl: IntlShape) {
|
protected getLabelValue(_intl: IntlShape) {
|
||||||
const value = DurationUtils.secondsToString(this.value.value ?? 0);
|
const value = TextUtils.secondsToTimestamp(this.value.value ?? 0);
|
||||||
const value2 = DurationUtils.secondsToString(this.value.value2 ?? 0);
|
const value2 = TextUtils.secondsToTimestamp(this.value.value2 ?? 0);
|
||||||
if (
|
if (
|
||||||
this.modifier === CriterionModifier.Between ||
|
this.modifier === CriterionModifier.Between ||
|
||||||
this.modifier === CriterionModifier.NotBetween
|
this.modifier === CriterionModifier.NotBetween
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import TextUtils from "./text";
|
|
||||||
|
|
||||||
const secondsToString = (seconds: number) => {
|
|
||||||
let ret = TextUtils.secondsToTimestamp(seconds);
|
|
||||||
|
|
||||||
if (ret.startsWith("00:")) {
|
|
||||||
ret = ret.substring(3);
|
|
||||||
|
|
||||||
if (ret.startsWith("0")) {
|
|
||||||
ret = ret.substring(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stringToSeconds = (v?: string) => {
|
|
||||||
if (!v) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const splits = v.split(":");
|
|
||||||
|
|
||||||
if (splits.length > 3) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seconds = 0;
|
|
||||||
let factor = 1;
|
|
||||||
while (splits.length > 0) {
|
|
||||||
const thisSplit = splits.pop();
|
|
||||||
if (thisSplit === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thisInt = parseInt(thisSplit, 10);
|
|
||||||
if (Number.isNaN(thisInt)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
seconds += factor * thisInt;
|
|
||||||
factor *= 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
return seconds;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DurationUtils = {
|
|
||||||
secondsToString,
|
|
||||||
stringToSeconds,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DurationUtils;
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Form } from "react-bootstrap";
|
|
||||||
import { DurationInput } from "src/components/Shared/DurationInput";
|
|
||||||
import { FilterSelect } from "src/components/Shared/Select";
|
|
||||||
import DurationUtils from "./duration";
|
|
||||||
|
|
||||||
const renderTextArea = (options: {
|
|
||||||
value: string | undefined;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
as="textarea"
|
|
||||||
readOnly={!options.isEditing}
|
|
||||||
plaintext={!options.isEditing}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
||||||
options.onChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
value={options.value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderEditableText = (options: {
|
|
||||||
title?: string;
|
|
||||||
value?: string | number;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Form.Control
|
|
||||||
readOnly={!options.isEditing}
|
|
||||||
plaintext={!options.isEditing}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
options.onChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
typeof options.value === "number"
|
|
||||||
? options.value.toString()
|
|
||||||
: options.value
|
|
||||||
}
|
|
||||||
placeholder={options.title}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderInputGroup = (options: {
|
|
||||||
title?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
value: string | undefined;
|
|
||||||
isEditing: boolean;
|
|
||||||
url?: string;
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
}) => {
|
|
||||||
if (options.url && !options.isEditing) {
|
|
||||||
return (
|
|
||||||
<a href={options.url} target="_blank" rel="noopener noreferrer">
|
|
||||||
{options.value}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
readOnly={!options.isEditing}
|
|
||||||
plaintext={!options.isEditing}
|
|
||||||
value={options.value ?? ""}
|
|
||||||
placeholder={options.placeholder ?? options.title}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (options.onChange) {
|
|
||||||
options.onChange(event.currentTarget.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderDurationInput = (options: {
|
|
||||||
value: number | undefined;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: number | undefined) => void;
|
|
||||||
}) => {
|
|
||||||
if (!options.isEditing) {
|
|
||||||
let durationString;
|
|
||||||
if (options.value !== undefined) {
|
|
||||||
durationString = DurationUtils.secondsToString(options.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
readOnly
|
|
||||||
plaintext
|
|
||||||
defaultValue={durationString}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DurationInput
|
|
||||||
value={options.value}
|
|
||||||
setValue={(v) => options.onChange(v)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHtmlSelect = (options: {
|
|
||||||
value?: string | number;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
selectOptions: Array<string | number>;
|
|
||||||
}) => {
|
|
||||||
if (!options.isEditing) {
|
|
||||||
return (
|
|
||||||
<Form.Control
|
|
||||||
className="text-input"
|
|
||||||
readOnly
|
|
||||||
plaintext
|
|
||||||
defaultValue={options.value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Form.Control
|
|
||||||
as="select"
|
|
||||||
className="input-control"
|
|
||||||
disabled={!options.isEditing}
|
|
||||||
plaintext={!options.isEditing}
|
|
||||||
value={options.value?.toString()}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
|
||||||
options.onChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{options.selectOptions.map((opt) => (
|
|
||||||
<option value={opt} key={opt}>
|
|
||||||
{opt}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: isediting
|
|
||||||
const renderFilterSelect = (options: {
|
|
||||||
type: "performers" | "studios" | "tags";
|
|
||||||
initialId: string | undefined;
|
|
||||||
onChange: (id: string | undefined) => void;
|
|
||||||
}) => (
|
|
||||||
<FilterSelect
|
|
||||||
type={options.type}
|
|
||||||
onSelect={(items) => options.onChange(items[0]?.id)}
|
|
||||||
initialIds={options.initialId ? [options.initialId] : []}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: isediting
|
|
||||||
const renderMultiSelect = (options: {
|
|
||||||
type: "performers" | "studios" | "tags";
|
|
||||||
initialIds: string[] | undefined;
|
|
||||||
onChange: (ids: string[]) => void;
|
|
||||||
}) => (
|
|
||||||
<FilterSelect
|
|
||||||
type={options.type}
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) => options.onChange(items.map((i) => i.id))}
|
|
||||||
initialIds={options.initialIds ?? []}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const EditableTextUtils = {
|
|
||||||
renderTextArea,
|
|
||||||
renderEditableText,
|
|
||||||
renderInputGroup,
|
|
||||||
renderDurationInput,
|
|
||||||
renderHtmlSelect,
|
|
||||||
renderFilterSelect,
|
|
||||||
renderMultiSelect,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditableTextUtils;
|
|
||||||
@@ -1,5 +1,22 @@
|
|||||||
import { Form, Col, Row, ColProps, FormLabelProps } from "react-bootstrap";
|
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import EditableTextUtils from "./editabletext";
|
import { FormikValues, useFormik } from "formik";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
ColProps,
|
||||||
|
Form,
|
||||||
|
FormLabelProps,
|
||||||
|
Row,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import { IntlShape } from "react-intl";
|
||||||
|
import { DateInput } from "src/components/Shared/DateInput";
|
||||||
|
import { DurationInput } from "src/components/Shared/DurationInput";
|
||||||
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
|
import { LinkType, StashIDPill } from "src/components/Shared/StashID";
|
||||||
|
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||||
|
import { URLListInput } from "src/components/Shared/URLField";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
function getLabelProps(labelProps?: FormLabelProps) {
|
function getLabelProps(labelProps?: FormLabelProps) {
|
||||||
let ret = labelProps;
|
let ret = labelProps;
|
||||||
@@ -13,155 +30,321 @@ function getLabelProps(labelProps?: FormLabelProps) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInputProps(inputProps?: ColProps) {
|
export function renderLabel(options: {
|
||||||
let ret = inputProps;
|
|
||||||
if (!ret) {
|
|
||||||
ret = {
|
|
||||||
xs: 9,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderLabel = (options: {
|
|
||||||
title: string;
|
title: string;
|
||||||
labelProps?: FormLabelProps;
|
labelProps?: FormLabelProps;
|
||||||
}) => (
|
}) {
|
||||||
|
return (
|
||||||
<Form.Label column {...getLabelProps(options.labelProps)}>
|
<Form.Label column {...getLabelProps(options.labelProps)}>
|
||||||
{options.title}
|
{options.title}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const renderEditableText = (options: {
|
type Formik<V extends FormikValues> = ReturnType<typeof useFormik<V>>;
|
||||||
title: string;
|
|
||||||
value?: string | number;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
labelProps?: FormLabelProps;
|
|
||||||
inputProps?: ColProps;
|
|
||||||
}) => (
|
|
||||||
<Form.Group controlId={options.title} as={Row}>
|
|
||||||
{renderLabel(options)}
|
|
||||||
<Col {...getInputProps(options.inputProps)}>
|
|
||||||
{EditableTextUtils.renderEditableText(options)}
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderTextArea = (options: {
|
interface IProps {
|
||||||
title: string;
|
|
||||||
value: string | undefined;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
labelProps?: FormLabelProps;
|
labelProps?: FormLabelProps;
|
||||||
inputProps?: ColProps;
|
fieldProps?: ColProps;
|
||||||
}) => (
|
}
|
||||||
<Form.Group controlId={options.title} as={Row}>
|
|
||||||
{renderLabel(options)}
|
|
||||||
<Col {...getInputProps(options.inputProps)}>
|
|
||||||
{EditableTextUtils.renderTextArea(options)}
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderInputGroup = (options: {
|
export function formikUtils<V extends FormikValues>(
|
||||||
title: string;
|
intl: IntlShape,
|
||||||
placeholder?: string;
|
formik: Formik<V>,
|
||||||
value: string | undefined;
|
{
|
||||||
isEditing: boolean;
|
labelProps = {
|
||||||
url?: string;
|
column: true,
|
||||||
onChange: (value: string) => void;
|
sm: 3,
|
||||||
labelProps?: FormLabelProps;
|
xl: 2,
|
||||||
inputProps?: ColProps;
|
},
|
||||||
}) => (
|
fieldProps = {
|
||||||
<Form.Group controlId={options.title} as={Row}>
|
sm: 9,
|
||||||
{renderLabel(options)}
|
xl: 7,
|
||||||
<Col {...getInputProps(options.inputProps)}>
|
},
|
||||||
{EditableTextUtils.renderInputGroup(options)}
|
}: IProps = {}
|
||||||
</Col>
|
) {
|
||||||
</Form.Group>
|
type Field = keyof V & string;
|
||||||
);
|
|
||||||
|
function renderFormControl(field: Field, type: string, placeholder: string) {
|
||||||
|
const formikProps = formik.getFieldProps({ name: field, type: type });
|
||||||
|
const { error } = formik.getFieldMeta(field);
|
||||||
|
|
||||||
|
let { value } = formikProps;
|
||||||
|
if (value === null) {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let control: React.ReactNode;
|
||||||
|
if (type === "checkbox") {
|
||||||
|
control = (
|
||||||
|
<Form.Check
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...formikProps}
|
||||||
|
value={value}
|
||||||
|
isInvalid={!!error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === "textarea") {
|
||||||
|
control = (
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
className="text-input"
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...formikProps}
|
||||||
|
value={value}
|
||||||
|
isInvalid={!!error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
control = (
|
||||||
|
<Form.Control
|
||||||
|
type={type}
|
||||||
|
className="text-input"
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...formikProps}
|
||||||
|
value={value}
|
||||||
|
isInvalid={!!error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const renderDurationInput = (options: {
|
|
||||||
title: string;
|
|
||||||
placeholder?: string;
|
|
||||||
value: number | undefined;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: number | undefined) => void;
|
|
||||||
labelProps?: FormLabelProps;
|
|
||||||
inputProps?: ColProps;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Form.Group controlId={options.title} as={Row}>
|
<>
|
||||||
{renderLabel(options)}
|
{control}
|
||||||
<Col {...getInputProps(options.inputProps)}>
|
<Form.Control.Feedback type="invalid">{error}</Form.Control.Feedback>
|
||||||
{EditableTextUtils.renderDurationInput(options)}
|
</>
|
||||||
</Col>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderField(
|
||||||
|
field: Field,
|
||||||
|
title: string,
|
||||||
|
control: React.ReactNode,
|
||||||
|
props?: IProps
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={field} as={Row}>
|
||||||
|
<Form.Label {...(props?.labelProps ?? labelProps)}>{title}</Form.Label>
|
||||||
|
<Col {...(props?.fieldProps ?? fieldProps)}>{control}</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const renderHtmlSelect = (options: {
|
function renderInputField(
|
||||||
title: string;
|
field: Field,
|
||||||
value?: string | number;
|
type: string = "text",
|
||||||
isEditing: boolean;
|
messageID: string = field,
|
||||||
onChange: (value: string) => void;
|
props?: IProps
|
||||||
selectOptions: Array<string | number>;
|
) {
|
||||||
labelProps?: FormLabelProps;
|
const title = intl.formatMessage({ id: messageID });
|
||||||
inputProps?: ColProps;
|
const control = renderFormControl(field, type, title);
|
||||||
}) => (
|
|
||||||
<Form.Group controlId={options.title} as={Row}>
|
|
||||||
{renderLabel(options)}
|
|
||||||
<Col {...getInputProps(options.inputProps)}>
|
|
||||||
{EditableTextUtils.renderHtmlSelect(options)}
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: isediting
|
return renderField(field, title, control, props);
|
||||||
const renderFilterSelect = (options: {
|
}
|
||||||
title: string;
|
|
||||||
type: "performers" | "studios" | "tags";
|
|
||||||
initialId: string | undefined;
|
|
||||||
onChange: (id: string | undefined) => void;
|
|
||||||
labelProps?: FormLabelProps;
|
|
||||||
inputProps?: ColProps;
|
|
||||||
}) => (
|
|
||||||
<Form.Group controlId={options.title} as={Row}>
|
|
||||||
{renderLabel(options)}
|
|
||||||
<Col {...getInputProps(options.inputProps)}>
|
|
||||||
{EditableTextUtils.renderFilterSelect(options)}
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: isediting
|
function renderSelectField(
|
||||||
const renderMultiSelect = (options: {
|
field: Field,
|
||||||
title: string;
|
entries: Map<string, string>,
|
||||||
type: "performers" | "studios" | "tags";
|
messageID: string = field,
|
||||||
initialIds: string[] | undefined;
|
props?: IProps
|
||||||
onChange: (ids: string[]) => void;
|
) {
|
||||||
labelProps?: FormLabelProps;
|
const formikProps = formik.getFieldProps(field);
|
||||||
inputProps?: ColProps;
|
|
||||||
}) => (
|
|
||||||
<Form.Group controlId={options.title} as={Row}>
|
|
||||||
{renderLabel(options)}
|
|
||||||
<Col {...getInputProps(options.inputProps)}>
|
|
||||||
{EditableTextUtils.renderMultiSelect(options)}
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
|
|
||||||
const FormUtils = {
|
let { value } = formikProps;
|
||||||
renderLabel,
|
if (value === null) {
|
||||||
renderEditableText,
|
value = "";
|
||||||
renderTextArea,
|
}
|
||||||
renderInputGroup,
|
|
||||||
renderDurationInput,
|
|
||||||
renderHtmlSelect,
|
|
||||||
renderFilterSelect,
|
|
||||||
renderMultiSelect,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormUtils;
|
const title = intl.formatMessage({ id: messageID });
|
||||||
|
const control = (
|
||||||
|
<Form.Control
|
||||||
|
as="select"
|
||||||
|
className="input-control"
|
||||||
|
{...formikProps}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
<option value="" key=""></option>
|
||||||
|
{Array.from(entries).map(([k, v]) => (
|
||||||
|
<option value={v} key={v}>
|
||||||
|
{k}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField(field, title, control, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDateField(
|
||||||
|
field: Field,
|
||||||
|
messageID: string = field,
|
||||||
|
props?: IProps
|
||||||
|
) {
|
||||||
|
const value = formik.values[field] as string;
|
||||||
|
const { error } = formik.getFieldMeta(field);
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ id: messageID });
|
||||||
|
const control = (
|
||||||
|
<DateInput
|
||||||
|
value={value}
|
||||||
|
onValueChange={(v) => formik.setFieldValue(field, v)}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField(field, title, control, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDurationField(
|
||||||
|
field: Field,
|
||||||
|
messageID: string = field,
|
||||||
|
props?: IProps
|
||||||
|
) {
|
||||||
|
const value = formik.values[field] as number | null;
|
||||||
|
const { error } = formik.getFieldMeta(field);
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ id: messageID });
|
||||||
|
const control = (
|
||||||
|
<DurationInput
|
||||||
|
value={value}
|
||||||
|
setValue={(v) => formik.setFieldValue(field, v)}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField(field, title, control, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRatingField(
|
||||||
|
field: Field,
|
||||||
|
messageID: string = field,
|
||||||
|
props?: IProps
|
||||||
|
) {
|
||||||
|
const value = formik.values[field] as number | null;
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ id: messageID });
|
||||||
|
const control = (
|
||||||
|
<RatingSystem
|
||||||
|
value={value}
|
||||||
|
onSetRating={(v) => formik.setFieldValue(field, v)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField(field, title, control, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStringListField(
|
||||||
|
field: Field,
|
||||||
|
errorMessageID: string,
|
||||||
|
messageID: string = field,
|
||||||
|
props?: IProps
|
||||||
|
) {
|
||||||
|
const formikProps = formik.getFieldProps(field);
|
||||||
|
const { error } = formik.getFieldMeta(field);
|
||||||
|
|
||||||
|
const errorMsg = error
|
||||||
|
? intl.formatMessage({ id: errorMessageID })
|
||||||
|
: undefined;
|
||||||
|
const errorIdx = error?.split(" ").map((e) => parseInt(e));
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ id: messageID });
|
||||||
|
const control = (
|
||||||
|
<StringListInput
|
||||||
|
value={formikProps.value ?? []}
|
||||||
|
setValue={(v) => formik.setFieldValue(field, v)}
|
||||||
|
errors={errorMsg}
|
||||||
|
errorIdx={errorIdx}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField(field, title, control, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderURLListField(
|
||||||
|
field: Field,
|
||||||
|
errorMessageID: string,
|
||||||
|
onScrapeClick?: (url: string) => void,
|
||||||
|
urlScrapable?: (url: string) => boolean,
|
||||||
|
messageID: string = field,
|
||||||
|
props?: IProps
|
||||||
|
) {
|
||||||
|
const value = formik.values[field] as string[];
|
||||||
|
const { error } = formik.getFieldMeta(field);
|
||||||
|
|
||||||
|
const errorMsg = error
|
||||||
|
? intl.formatMessage({ id: errorMessageID })
|
||||||
|
: undefined;
|
||||||
|
const errorIdx = error?.split(" ").map((e) => parseInt(e));
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ id: messageID });
|
||||||
|
const control = (
|
||||||
|
<URLListInput
|
||||||
|
value={value}
|
||||||
|
setValue={(v) => formik.setFieldValue(field, v)}
|
||||||
|
errors={errorMsg}
|
||||||
|
errorIdx={errorIdx}
|
||||||
|
onScrapeClick={onScrapeClick}
|
||||||
|
urlScrapable={urlScrapable}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField(field, title, control, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStashIDsField(
|
||||||
|
field: Field,
|
||||||
|
linkType: LinkType,
|
||||||
|
messageID: string = field,
|
||||||
|
props?: IProps
|
||||||
|
) {
|
||||||
|
const values = formik.values[field] as GQL.StashIdInput[];
|
||||||
|
if (!values.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ id: messageID });
|
||||||
|
|
||||||
|
const removeStashID = (stashID: GQL.StashIdInput) => {
|
||||||
|
const v = values.filter((s) => s !== stashID);
|
||||||
|
formik.setFieldValue(field, v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const control = (
|
||||||
|
<ul className="pl-0 mb-0">
|
||||||
|
{values.map((stashID) => {
|
||||||
|
return (
|
||||||
|
<Row as="li" key={stashID.stash_id} noGutters>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
className="mr-2 py-0"
|
||||||
|
title={intl.formatMessage(
|
||||||
|
{ id: "actions.delete_entity" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "stash_id" }) }
|
||||||
|
)}
|
||||||
|
onClick={() => removeStashID(stashID)}
|
||||||
|
>
|
||||||
|
<Icon icon={faTrashAlt} />
|
||||||
|
</Button>
|
||||||
|
<StashIDPill stashID={stashID} linkType={linkType} />
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderField(field, title, control, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderFormControl,
|
||||||
|
renderField,
|
||||||
|
renderInputField,
|
||||||
|
renderSelectField,
|
||||||
|
renderDateField,
|
||||||
|
renderDurationField,
|
||||||
|
renderRatingField,
|
||||||
|
renderStringListField,
|
||||||
|
renderURLListField,
|
||||||
|
renderStashIDsField,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function getRatingPrecision(precision: RatingStarPrecision) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function convertToRatingFormat(
|
export function convertToRatingFormat(
|
||||||
rating: number | undefined,
|
rating: number | null | undefined,
|
||||||
ratingSystemOptions: RatingSystemOptions
|
ratingSystemOptions: RatingSystemOptions
|
||||||
) {
|
) {
|
||||||
if (!rating) {
|
if (!rating) {
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import EditableTextUtils from "./editabletext";
|
|
||||||
|
|
||||||
const renderEditableTextTableRow = (options: {
|
|
||||||
title: string;
|
|
||||||
value?: string | number;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}) => (
|
|
||||||
<tr>
|
|
||||||
<td>{options.title}</td>
|
|
||||||
<td>{EditableTextUtils.renderEditableText(options)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderTextArea = (options: {
|
|
||||||
title: string;
|
|
||||||
value: string | undefined;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}) => (
|
|
||||||
<tr>
|
|
||||||
<td>{options.title}</td>
|
|
||||||
<td>{EditableTextUtils.renderTextArea(options)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderInputGroup = (options: {
|
|
||||||
title: string;
|
|
||||||
placeholder?: string;
|
|
||||||
value: string | undefined;
|
|
||||||
isEditing: boolean;
|
|
||||||
url?: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
}) => (
|
|
||||||
<tr>
|
|
||||||
<td>{options.title}</td>
|
|
||||||
<td>{EditableTextUtils.renderInputGroup(options)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderDurationInput = (options: {
|
|
||||||
title: string;
|
|
||||||
placeholder?: string;
|
|
||||||
value: number | undefined;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: number | undefined) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td>{options.title}</td>
|
|
||||||
<td>{EditableTextUtils.renderDurationInput(options)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHtmlSelect = (options: {
|
|
||||||
title: string;
|
|
||||||
value?: string | number;
|
|
||||||
isEditing: boolean;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
selectOptions: Array<string | number>;
|
|
||||||
}) => (
|
|
||||||
<tr>
|
|
||||||
<td>{options.title}</td>
|
|
||||||
<td>{EditableTextUtils.renderHtmlSelect(options)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: isediting
|
|
||||||
const renderFilterSelect = (options: {
|
|
||||||
title: string;
|
|
||||||
type: "performers" | "studios" | "tags";
|
|
||||||
initialId: string | undefined;
|
|
||||||
onChange: (id: string | undefined) => void;
|
|
||||||
}) => (
|
|
||||||
<tr>
|
|
||||||
<td>{options.title}</td>
|
|
||||||
<td>{EditableTextUtils.renderFilterSelect(options)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: isediting
|
|
||||||
const renderMultiSelect = (options: {
|
|
||||||
title: string;
|
|
||||||
type: "performers" | "studios" | "tags";
|
|
||||||
initialIds: string[] | undefined;
|
|
||||||
onChange: (ids: string[]) => void;
|
|
||||||
}) => (
|
|
||||||
<tr>
|
|
||||||
<td>{options.title}</td>
|
|
||||||
<td>{EditableTextUtils.renderMultiSelect(options)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
const TableUtils = {
|
|
||||||
renderEditableTextTableRow,
|
|
||||||
renderTextArea,
|
|
||||||
renderInputGroup,
|
|
||||||
renderDurationInput,
|
|
||||||
renderHtmlSelect,
|
|
||||||
renderFilterSelect,
|
|
||||||
renderMultiSelect,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableUtils;
|
|
||||||
@@ -129,16 +129,11 @@ const secondsAsTime = (seconds: number = 0): DurationCount[] => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeAsString = (time: DurationCount[]): string => {
|
|
||||||
return time.join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const secondsAsTimeString = (
|
const secondsAsTimeString = (
|
||||||
seconds: number = 0,
|
seconds: number = 0,
|
||||||
maxUnitCount: number = 2
|
maxUnitCount: number = 2
|
||||||
): string => {
|
): string => {
|
||||||
const timeArray = secondsAsTime(seconds).slice(0, maxUnitCount);
|
return secondsAsTime(seconds).slice(0, maxUnitCount).join(" ");
|
||||||
return timeAsString(timeArray);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatFileSizeUnit = (u: Unit) => {
|
const formatFileSizeUnit = (u: Unit) => {
|
||||||
@@ -156,18 +151,68 @@ const fileSizeFractionalDigits = (unit: Unit) => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Converts seconds to a hh:mm:ss or mm:ss timestamp.
|
||||||
|
// A negative input will result in a -hh:mm:ss or -mm:ss output.
|
||||||
|
// Fractional inputs are truncated.
|
||||||
const secondsToTimestamp = (seconds: number) => {
|
const secondsToTimestamp = (seconds: number) => {
|
||||||
let ret = new Date(seconds * 1000).toISOString().substring(11, 19);
|
let neg = false;
|
||||||
|
if (seconds < 0) {
|
||||||
|
neg = true;
|
||||||
|
seconds = -seconds;
|
||||||
|
}
|
||||||
|
seconds = Math.trunc(seconds);
|
||||||
|
|
||||||
if (ret.startsWith("00")) {
|
const s = seconds % 60;
|
||||||
// strip hours if under one hour
|
seconds = (seconds - s) / 60;
|
||||||
ret = ret.substring(3);
|
|
||||||
}
|
const m = seconds % 60;
|
||||||
if (ret.startsWith("0")) {
|
seconds = (seconds - m) / 60;
|
||||||
// for duration under a minute, leave one leading zero
|
|
||||||
ret = ret.substring(1);
|
const h = seconds;
|
||||||
|
|
||||||
|
let ret = String(s).padStart(2, "0");
|
||||||
|
if (h === 0) {
|
||||||
|
ret = String(m) + ":" + ret;
|
||||||
|
} else {
|
||||||
|
ret = String(m).padStart(2, "0") + ":" + ret;
|
||||||
|
ret = String(h) + ":" + ret;
|
||||||
}
|
}
|
||||||
|
if (neg) {
|
||||||
|
return "-" + ret;
|
||||||
|
} else {
|
||||||
return ret;
|
return ret;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timestampToSeconds = (v: string | null | undefined) => {
|
||||||
|
if (!v) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splits = v.split(":");
|
||||||
|
|
||||||
|
if (splits.length > 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let seconds = 0;
|
||||||
|
let factor = 1;
|
||||||
|
while (splits.length > 0) {
|
||||||
|
const thisSplit = splits.pop();
|
||||||
|
if (thisSplit === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thisInt = parseInt(thisSplit, 10);
|
||||||
|
if (Number.isNaN(thisInt)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds += factor * thisInt;
|
||||||
|
factor *= 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
return seconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileNameFromPath = (path: string) => {
|
const fileNameFromPath = (path: string) => {
|
||||||
@@ -415,6 +460,7 @@ const TextUtils = {
|
|||||||
formatFileSizeUnit,
|
formatFileSizeUnit,
|
||||||
fileSizeFractionalDigits,
|
fileSizeFractionalDigits,
|
||||||
secondsToTimestamp,
|
secondsToTimestamp,
|
||||||
|
timestampToSeconds,
|
||||||
fileNameFromPath,
|
fileNameFromPath,
|
||||||
stringToDate,
|
stringToDate,
|
||||||
stringToFuzzyDate,
|
stringToFuzzyDate,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FormikErrors, yupToFormErrors } from "formik";
|
||||||
import { IntlShape } from "react-intl";
|
import { IntlShape } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
@@ -8,15 +9,39 @@ export function yupUniqueStringList(fieldName: string) {
|
|||||||
.test({
|
.test({
|
||||||
name: "unique",
|
name: "unique",
|
||||||
test: (value) => {
|
test: (value) => {
|
||||||
const dupes = value
|
const values: string[] = [];
|
||||||
.map((e, i, a) => {
|
const dupes: number[] = [];
|
||||||
if (a.indexOf(e) !== i) {
|
for (let i = 0; i < value.length; i++) {
|
||||||
return String(i - 1);
|
const a = value[i];
|
||||||
|
if (values.includes(a)) {
|
||||||
|
dupes.push(i);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
values.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dupes.length === 0) return true;
|
||||||
|
return new yup.ValidationError(dupes.join(" "), value, fieldName);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function yupUniqueAliases(fieldName: string, nameField: string) {
|
||||||
|
return yup
|
||||||
|
.array(yup.string().required())
|
||||||
|
.defined()
|
||||||
|
.test({
|
||||||
|
name: "unique",
|
||||||
|
test: (value, context) => {
|
||||||
|
const aliases = [context.parent[nameField].toLowerCase()];
|
||||||
|
const dupes: number[] = [];
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const a = value[i].toLowerCase();
|
||||||
|
if (aliases.includes(a)) {
|
||||||
|
dupes.push(i);
|
||||||
|
} else {
|
||||||
|
aliases.push(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.filter((e) => e !== null) as string[];
|
|
||||||
if (dupes.length === 0) return true;
|
if (dupes.length === 0) return true;
|
||||||
return new yup.ValidationError(dupes.join(" "), value, fieldName);
|
return new yup.ValidationError(dupes.join(" "), value, fieldName);
|
||||||
},
|
},
|
||||||
@@ -38,3 +63,42 @@ export function yupDateString(intl: IntlShape) {
|
|||||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StringEnum<T extends string> = {
|
||||||
|
[k: string]: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use yupInputEnum to validate a string enum from a <select>.
|
||||||
|
// If "" is not a value in the enum, a "" input will be transformed to null.
|
||||||
|
export function yupInputEnum<T extends string>(e: StringEnum<T>) {
|
||||||
|
const enumValues = Object.values(e);
|
||||||
|
const schema = yup.string<T>().oneOf(enumValues);
|
||||||
|
if (enumValues.includes("" as T)) {
|
||||||
|
return schema;
|
||||||
|
} else {
|
||||||
|
return schema.transform((v, o) => (o === "" ? null : v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use yupInputNumber to validate a number from an <input type="number">.
|
||||||
|
// A "" input will be transformed to null.
|
||||||
|
export function yupInputNumber() {
|
||||||
|
return yup.number().transform((v, o) => (o === "" ? null : v));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formik converts "" into undefined when validating with a yup schema,
|
||||||
|
// which prevents transformations from running.
|
||||||
|
// Interfacing with yup ourselves avoids this.
|
||||||
|
// https://github.com/jaredpalmer/formik/pull/2902#issuecomment-922492137
|
||||||
|
export function yupFormikValidate<T>(
|
||||||
|
schema: yup.AnySchema
|
||||||
|
): (values: T) => Promise<FormikErrors<T>> {
|
||||||
|
return async function (values) {
|
||||||
|
try {
|
||||||
|
await schema.validate(values, { abortEarly: false });
|
||||||
|
} catch (err) {
|
||||||
|
return yupToFormErrors(err);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -4489,18 +4489,19 @@ form-data@^3.0.0:
|
|||||||
combined-stream "^1.0.8"
|
combined-stream "^1.0.8"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
formik@^2.2.9:
|
formik@^2.4.5:
|
||||||
version "2.2.9"
|
version "2.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0"
|
resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.5.tgz#f899b5b7a6f103a8fabb679823e8fafc7e0ee1b4"
|
||||||
integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==
|
integrity sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@types/hoist-non-react-statics" "^3.3.1"
|
||||||
deepmerge "^2.1.1"
|
deepmerge "^2.1.1"
|
||||||
hoist-non-react-statics "^3.3.0"
|
hoist-non-react-statics "^3.3.0"
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
lodash-es "^4.17.21"
|
lodash-es "^4.17.21"
|
||||||
react-fast-compare "^2.0.1"
|
react-fast-compare "^2.0.1"
|
||||||
tiny-warning "^1.0.2"
|
tiny-warning "^1.0.2"
|
||||||
tslib "^1.10.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
fs-extra@^10.0.0:
|
fs-extra@^10.0.0:
|
||||||
version "10.1.0"
|
version "10.1.0"
|
||||||
@@ -7792,7 +7793,7 @@ tsconfig-paths@^3.14.1:
|
|||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
strip-bom "^3.0.0"
|
strip-bom "^3.0.0"
|
||||||
|
|
||||||
tslib@^1.10.0, tslib@^1.8.1:
|
tslib@^1.8.1:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
@@ -8503,10 +8504,10 @@ yocto-queue@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
yup@^1.0.0:
|
yup@^1.3.2:
|
||||||
version "1.0.0"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/yup/-/yup-1.0.0.tgz#de4e32f9d2e45b1ab428076fc916c84db861b8ce"
|
resolved "https://registry.yarnpkg.com/yup/-/yup-1.3.2.tgz#afffc458f1513ed386e6aaf4bcaa4e67a9e270dc"
|
||||||
integrity sha512-bRZIyMkoe212ahGJTE32cr2dLkJw53Va+Uw5mzsBKpcef9zCGQ23k/xtpQUfGwdWPKvCIlR8CzFwchs2rm2XpQ==
|
integrity sha512-6KCM971iQtJ+/KUaHdrhVr2LDkfhBtFPRnsG1P8F4q3uUVQ2RfEM9xekpha9aA4GXWJevjM10eDcPQ1FfWlmaQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
property-expr "^2.0.5"
|
property-expr "^2.0.5"
|
||||||
tiny-case "^1.0.3"
|
tiny-case "^1.0.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user