mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Allow string list input to be orderable (#6397)
* Allow string list input to be orderable * Make alias fields not orderable * Adjust styling for URL list controls
This commit is contained in:
@@ -350,6 +350,19 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
xl: 12,
|
xl: 12,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const urlProps = isNew
|
||||||
|
? splitProps
|
||||||
|
: {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
md: 3,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
md: 9,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
const { renderField, renderInputField, renderDateField, renderURLListField } =
|
const { renderField, renderInputField, renderDateField, renderURLListField } =
|
||||||
formikUtils(intl, formik, splitProps);
|
formikUtils(intl, formik, splitProps);
|
||||||
|
|
||||||
@@ -466,7 +479,13 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
{renderInputField("title")}
|
{renderInputField("title")}
|
||||||
{renderInputField("code", "text", "scene_code")}
|
{renderInputField("code", "text", "scene_code")}
|
||||||
|
|
||||||
{renderURLListField("urls", onScrapeGalleryURL, urlScrapable)}
|
{renderURLListField(
|
||||||
|
"urls",
|
||||||
|
onScrapeGalleryURL,
|
||||||
|
urlScrapable,
|
||||||
|
"urls",
|
||||||
|
urlProps
|
||||||
|
)}
|
||||||
|
|
||||||
{renderDateField("date")}
|
{renderDateField("date")}
|
||||||
{renderInputField("photographer")}
|
{renderInputField("photographer")}
|
||||||
|
|||||||
@@ -204,6 +204,10 @@ $galleryTabWidth: 450px;
|
|||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
height: calc(1.5em + 0.75rem + 2px);
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group[data-field="urls"] .string-list-input input.form-control {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-cover {
|
.gallery-cover {
|
||||||
|
|||||||
@@ -320,6 +320,19 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
xl: 12,
|
xl: 12,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const urlProps = isNew
|
||||||
|
? splitProps
|
||||||
|
: {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
md: 3,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
md: 9,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
const { renderField, renderInputField, renderDateField, renderURLListField } =
|
const { renderField, renderInputField, renderDateField, renderURLListField } =
|
||||||
formikUtils(intl, formik, splitProps);
|
formikUtils(intl, formik, splitProps);
|
||||||
|
|
||||||
@@ -461,7 +474,13 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
{renderInputField("title")}
|
{renderInputField("title")}
|
||||||
{renderInputField("code", "text", "scene_code")}
|
{renderInputField("code", "text", "scene_code")}
|
||||||
|
|
||||||
{renderURLListField("urls", onScrapeImageURL, urlScrapable)}
|
{renderURLListField(
|
||||||
|
"urls",
|
||||||
|
onScrapeImageURL,
|
||||||
|
urlScrapable,
|
||||||
|
"urls",
|
||||||
|
urlProps
|
||||||
|
)}
|
||||||
|
|
||||||
{renderDateField("date")}
|
{renderDateField("date")}
|
||||||
{renderInputField("photographer")}
|
{renderInputField("photographer")}
|
||||||
|
|||||||
@@ -175,6 +175,10 @@ $imageTabWidth: 450px;
|
|||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
height: calc(1.5em + 0.75rem + 2px);
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group[data-field="urls"] .string-list-input input.form-control {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-file-card.card {
|
.image-file-card.card {
|
||||||
|
|||||||
@@ -694,7 +694,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
{renderInputField("name")}
|
{renderInputField("name")}
|
||||||
{renderInputField("disambiguation")}
|
{renderInputField("disambiguation")}
|
||||||
|
|
||||||
{renderStringListField("alias_list", "aliases")}
|
{renderStringListField("alias_list", "aliases", { orderable: false })}
|
||||||
|
|
||||||
{renderSelectField("gender", stringGenderMap)}
|
{renderSelectField("gender", stringGenderMap)}
|
||||||
|
|
||||||
|
|||||||
@@ -602,6 +602,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
xl: 12,
|
xl: 12,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const urlProps = isNew
|
||||||
|
? splitProps
|
||||||
|
: {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
md: 3,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
md: 9,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
const {
|
const {
|
||||||
renderField,
|
renderField,
|
||||||
renderInputField,
|
renderInputField,
|
||||||
@@ -770,7 +783,13 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
{renderInputField("title")}
|
{renderInputField("title")}
|
||||||
{renderInputField("code", "text", "scene_code")}
|
{renderInputField("code", "text", "scene_code")}
|
||||||
|
|
||||||
{renderURLListField("urls", onScrapeSceneURL, urlScrapable)}
|
{renderURLListField(
|
||||||
|
"urls",
|
||||||
|
onScrapeSceneURL,
|
||||||
|
urlScrapable,
|
||||||
|
"urls",
|
||||||
|
urlProps
|
||||||
|
)}
|
||||||
|
|
||||||
{renderDateField("date")}
|
{renderDateField("date")}
|
||||||
{renderInputField("director")}
|
{renderInputField("director")}
|
||||||
|
|||||||
@@ -558,6 +558,10 @@ input[type="range"].blue-slider {
|
|||||||
top: 3rem;
|
top: 3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group[data-field="urls"] .string-list-input input.form-control {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-markers-panel {
|
.scene-markers-panel {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
import { faGripVertical, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import React, { ComponentType } from "react";
|
import React, { ComponentType, useState } from "react";
|
||||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
@@ -25,6 +25,8 @@ export interface IStringListInputProps {
|
|||||||
errors?: string;
|
errors?: string;
|
||||||
errorIdx?: number[];
|
errorIdx?: number[];
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
// defaults to true if not set
|
||||||
|
orderable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StringInput: React.FC<IListInputComponentProps> = ({
|
export const StringInput: React.FC<IListInputComponentProps> = ({
|
||||||
@@ -51,6 +53,9 @@ export const StringListInput: React.FC<IStringListInputProps> = (props) => {
|
|||||||
const Input = props.inputComponent ?? StringInput;
|
const Input = props.inputComponent ?? StringInput;
|
||||||
const AppendComponent = props.appendComponent;
|
const AppendComponent = props.appendComponent;
|
||||||
const values = props.value.concat("");
|
const values = props.value.concat("");
|
||||||
|
const [draggedIdx, setDraggedIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const { orderable = true } = props;
|
||||||
|
|
||||||
function valueChanged(idx: number, value: string) {
|
function valueChanged(idx: number, value: string) {
|
||||||
const newValues = props.value.slice();
|
const newValues = props.value.slice();
|
||||||
@@ -70,12 +75,46 @@ export const StringListInput: React.FC<IStringListInputProps> = (props) => {
|
|||||||
props.setValue(newValues);
|
props.setValue(newValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDragStart(event: React.DragEvent<HTMLElement>, idx: number) {
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
setDraggedIdx(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(e: React.DragEvent, idx: number) {
|
||||||
|
e.dataTransfer.dropEffect = "move";
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (
|
||||||
|
draggedIdx === null ||
|
||||||
|
draggedIdx === idx ||
|
||||||
|
idx === values.length - 1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValues = [...props.value];
|
||||||
|
const draggedValue = newValues[draggedIdx];
|
||||||
|
newValues.splice(draggedIdx, 1);
|
||||||
|
newValues.splice(idx, 0, draggedValue);
|
||||||
|
|
||||||
|
props.setValue(newValues);
|
||||||
|
setDraggedIdx(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
setDraggedIdx(null);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`string-list-input ${props.errors ? "is-invalid" : ""}`}>
|
<div className={`string-list-input ${props.errors ? "is-invalid" : ""}`}>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
{values.map((v, i) => (
|
{values.map((v, i) => (
|
||||||
<InputGroup className={props.className} key={i}>
|
<InputGroup
|
||||||
|
className={props.className}
|
||||||
|
key={i}
|
||||||
|
onDragOver={(e) => handleDragOver(e, i)}
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
value={v}
|
value={v}
|
||||||
setValue={(value) => valueChanged(i, value)}
|
setValue={(value) => valueChanged(i, value)}
|
||||||
@@ -85,11 +124,24 @@ export const StringListInput: React.FC<IStringListInputProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
{AppendComponent && <AppendComponent value={v} />}
|
{AppendComponent && <AppendComponent value={v} />}
|
||||||
|
{!props.readOnly && values.length > 2 && orderable && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="drag-handle minimal"
|
||||||
|
draggable={i !== values.length - 1}
|
||||||
|
disabled={i === values.length - 1}
|
||||||
|
onDragStart={(e) => handleDragStart(e, i)}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<Icon icon={faGripVertical} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{!props.readOnly && (
|
{!props.readOnly && (
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => removeValue(i)}
|
onClick={() => removeValue(i)}
|
||||||
disabled={i === values.length - 1}
|
disabled={i === values.length - 1}
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<Icon icon={faMinus} />
|
<Icon icon={faMinus} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -412,6 +412,25 @@ button.collapse-button {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.drag-handle {
|
||||||
|
display: inline-block;
|
||||||
|
margin: -0.25em 0.25em -0.25em -0.25em;
|
||||||
|
padding: 0.25em 0.5em 0.25em;
|
||||||
|
|
||||||
|
&:not(:disabled):not(.disabled) {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:focus:active {
|
||||||
|
background-color: initial;
|
||||||
|
border-color: initial;
|
||||||
|
box-shadow: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-update-text-input {
|
.bulk-update-text-input {
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
|
<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
|
||||||
{renderInputField("name")}
|
{renderInputField("name")}
|
||||||
{renderInputField("sort_name", "text")}
|
{renderInputField("sort_name", "text")}
|
||||||
{renderStringListField("aliases")}
|
{renderStringListField("aliases", "aliases", { orderable: false })}
|
||||||
{renderInputField("description", "textarea")}
|
{renderInputField("description", "textarea")}
|
||||||
{renderParentTagsField()}
|
{renderParentTagsField()}
|
||||||
{renderSubTagsField()}
|
{renderSubTagsField()}
|
||||||
|
|||||||
@@ -308,10 +308,15 @@ export function formikUtils<V extends FormikValues>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IStringListProps extends IProps {
|
||||||
|
// defaults to true if not provided
|
||||||
|
orderable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function renderStringListField(
|
function renderStringListField(
|
||||||
field: Field,
|
field: Field,
|
||||||
messageID: string = field,
|
messageID: string = field,
|
||||||
props?: IProps
|
props?: IStringListProps
|
||||||
) {
|
) {
|
||||||
const value = formik.values[field] as string[];
|
const value = formik.values[field] as string[];
|
||||||
const error = formik.errors[field] as ErrorMessage[] | ErrorMessage;
|
const error = formik.errors[field] as ErrorMessage[] | ErrorMessage;
|
||||||
@@ -325,6 +330,7 @@ export function formikUtils<V extends FormikValues>(
|
|||||||
setValue={(v) => formik.setFieldValue(field, v)}
|
setValue={(v) => formik.setFieldValue(field, v)}
|
||||||
errors={errorMsg}
|
errors={errorMsg}
|
||||||
errorIdx={errorIdx}
|
errorIdx={errorIdx}
|
||||||
|
orderable={props?.orderable}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user