Skip to content

Commit

Permalink
Fix crash on blank aliases/urls (stashapp#4344)
Browse files Browse the repository at this point in the history
* Fix crash on blank alias/url
* Fix StringListInput clear issue
  • Loading branch information
DingDongSoLong4 authored and halkeye committed Sep 1, 2024
1 parent fe134c1 commit 74ed401
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
const schema = yup.object({
title: titleRequired ? yup.string().required() : yup.string().ensure(),
code: yup.string().ensure(),
urls: yupUniqueStringList("urls"),
urls: yupUniqueStringList(intl),
date: yupDateString(intl),
photographer: yup.string().ensure(),
rating100: yup.number().integer().nullable().defined(),
Expand Down Expand Up @@ -504,12 +504,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
{renderInputField("title")}
{renderInputField("code", "text", "scene_code")}

{renderURLListField(
"urls",
"validation.urls_must_be_unique",
onScrapeGalleryURL,
urlScrapable
)}
{renderURLListField("urls", onScrapeGalleryURL, urlScrapable)}

{renderDateField("date")}
{renderInputField("photographer")}
Expand Down
4 changes: 2 additions & 2 deletions ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
const schema = yup.object({
title: yup.string().ensure(),
code: yup.string().ensure(),
urls: yupUniqueStringList("urls"),
urls: yupUniqueStringList(intl),
date: yupDateString(intl),
details: yup.string().ensure(),
photographer: yup.string().ensure(),
Expand Down Expand Up @@ -258,7 +258,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
{renderInputField("title")}
{renderInputField("code", "text", "scene_code")}

{renderURLListField("urls", "validation.urls_must_be_unique")}
{renderURLListField("urls")}

{renderDateField("date")}
{renderInputField("photographer")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const schema = yup.object({
name: yup.string().required(),
disambiguation: yup.string().ensure(),
alias_list: yupUniqueAliases("alias_list", "name"),
alias_list: yupUniqueAliases(intl, "name"),
gender: yupInputEnum(GQL.GenderEnum).nullable().defined(),
birthdate: yupDateString(intl),
death_date: yupDateString(intl),
Expand Down Expand Up @@ -755,11 +755,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
{renderInputField("name")}
{renderInputField("disambiguation")}

{renderStringListField(
"alias_list",
"validation.aliases_must_be_unique",
"aliases"
)}
{renderStringListField("alias_list", "aliases")}

{renderSelectField("gender", stringGenderMap)}

Expand Down
9 changes: 2 additions & 7 deletions ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
const schema = yup.object({
title: yup.string().ensure(),
code: yup.string().ensure(),
urls: yupUniqueStringList("urls"),
urls: yupUniqueStringList(intl),
date: yupDateString(intl),
director: yup.string().ensure(),
rating100: yup.number().integer().nullable().defined(),
Expand Down Expand Up @@ -824,12 +824,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
{renderInputField("title")}
{renderInputField("code", "text", "scene_code")}

{renderURLListField(
"urls",
"validation.urls_must_be_unique",
onScrapeSceneURL,
urlScrapable
)}
{renderURLListField("urls", onScrapeSceneURL, urlScrapable)}

{renderDateField("date")}
{renderInputField("director")}
Expand Down
14 changes: 8 additions & 6 deletions ui/v2.5/src/components/Shared/StringListInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ export const StringListInput: React.FC<IStringListInputProps> = (props) => {
const values = props.value.concat("");

function valueChanged(idx: number, value: string) {
const newValues = values
.map((v, i) => {
const ret = idx !== i ? v : value;
return ret;
})
.filter((v, i) => i < values.length - 2 || v);
const newValues = props.value.slice();
newValues[idx] = value;

// if we cleared the last string, delete it from the array entirely
if (!value && idx === newValues.length - 1) {
newValues.splice(newValues.length - 1);
}

props.setValue(newValues);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
url: yup.string().ensure(),
details: yup.string().ensure(),
parent_id: yup.string().required().nullable(),
aliases: yupUniqueAliases("aliases", "name"),
aliases: yupUniqueAliases(intl, "name"),
ignore_auto_tag: yup.boolean().defined(),
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
image: yup.string().nullable().optional(),
Expand Down Expand Up @@ -158,7 +158,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({

<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
{renderInputField("name")}
{renderStringListField("aliases", "validation.aliases_must_be_unique")}
{renderStringListField("aliases")}
{renderInputField("url")}
{renderInputField("details", "textarea")}
{renderParentStudioField()}
Expand Down
4 changes: 2 additions & 2 deletions ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({

const schema = yup.object({
name: yup.string().required(),
aliases: yupUniqueAliases("aliases", "name"),
aliases: yupUniqueAliases(intl, "name"),
description: yup.string().ensure(),
parent_ids: yup.array(yup.string().required()).defined(),
child_ids: yup.array(yup.string().required()).defined(),
Expand Down Expand Up @@ -186,7 +186,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({

<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
{renderInputField("name")}
{renderStringListField("aliases", "validation.aliases_must_be_unique")}
{renderStringListField("aliases")}
{renderInputField("description", "textarea")}
{renderParentTagsField()}
{renderSubTagsField()}
Expand Down
1 change: 1 addition & 0 deletions ui/v2.5/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,7 @@ dl.details-list {

.invalid-feedback {
display: block;
white-space: pre-wrap;

&:empty {
display: none;
Expand Down
5 changes: 2 additions & 3 deletions ui/v2.5/src/locales/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -1390,11 +1390,10 @@
"url": "URL",
"urls": "URLs",
"validation": {
"aliases_must_be_unique": "aliases must be unique",
"blank": "${path} must not be blank",
"date_invalid_form": "${path} must be in YYYY-MM-DD form",
"required": "${path} is a required field",
"unique": "${path} must be unique",
"urls_must_be_unique": "URLs must be unique"
"unique": "${path} must be unique"
},
"videos": "Videos",
"video_codec": "Video Codec",
Expand Down
50 changes: 33 additions & 17 deletions ui/v2.5/src/utils/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ export function formikUtils<V extends FormikValues>(
}: IProps = {}
) {
type Field = keyof V & string;
type ErrorMessage = string | undefined;

function renderFormControl(field: Field, type: string, placeholder: string) {
const formikProps = formik.getFieldProps({ name: field, type: type });
const { error } = formik.getFieldMeta(field);
const error = formik.errors[field] as ErrorMessage;

let { value } = formikProps;
if (value === null) {
Expand Down Expand Up @@ -181,7 +182,7 @@ export function formikUtils<V extends FormikValues>(
props?: IProps
) {
const value = formik.values[field] as string;
const { error } = formik.getFieldMeta(field);
const error = formik.errors[field] as ErrorMessage;

const title = intl.formatMessage({ id: messageID });
const control = (
Expand All @@ -201,7 +202,7 @@ export function formikUtils<V extends FormikValues>(
props?: IProps
) {
const value = formik.values[field] as number | null;
const { error } = formik.getFieldMeta(field);
const error = formik.errors[field] as ErrorMessage;

const title = intl.formatMessage({ id: messageID });
const control = (
Expand Down Expand Up @@ -233,24 +234,43 @@ export function formikUtils<V extends FormikValues>(
return renderField(field, title, control, props);
}

// flattens a potential list of errors into a [errorMsg, errorIdx] tuple
// error messages are joined with newlines, and duplicate messages are skipped
function flattenError(
error: ErrorMessage[] | ErrorMessage
): [string | undefined, number[] | undefined] {
if (Array.isArray(error)) {
let errors: string[] = [];
const errorIdx = [];
for (let i = 0; i < error.length; i++) {
const err = error[i];
if (err) {
if (!errors.includes(err)) {
errors.push(err);
}
errorIdx.push(i);
}
}
return [errors.join("\n"), errorIdx];
} else {
return [error, undefined];
}
}

function renderStringListField(
field: Field,
errorMessageID: string,
messageID: string = field,
props?: IProps
) {
const formikProps = formik.getFieldProps(field);
const { error } = formik.getFieldMeta(field);
const value = formik.values[field] as string[];
const error = formik.errors[field] as ErrorMessage[] | ErrorMessage;

const errorMsg = error
? intl.formatMessage({ id: errorMessageID })
: undefined;
const errorIdx = error?.split(" ").map((e) => parseInt(e));
const [errorMsg, errorIdx] = flattenError(error);

const title = intl.formatMessage({ id: messageID });
const control = (
<StringListInput
value={formikProps.value ?? []}
value={value}
setValue={(v) => formik.setFieldValue(field, v)}
errors={errorMsg}
errorIdx={errorIdx}
Expand All @@ -262,19 +282,15 @@ export function formikUtils<V extends FormikValues>(

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 error = formik.errors[field] as ErrorMessage[] | ErrorMessage;

const errorMsg = error
? intl.formatMessage({ id: errorMessageID })
: undefined;
const errorIdx = error?.split(" ").map((e) => parseInt(e));
const [errorMsg, errorIdx] = flattenError(error);

const title = intl.formatMessage({ id: messageID });
const control = (
Expand Down
Loading

0 comments on commit 74ed401

Please sign in to comment.