mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +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,
|
||||
},
|
||||
};
|
||||
const urlProps = isNew
|
||||
? splitProps
|
||||
: {
|
||||
labelProps: {
|
||||
column: true,
|
||||
md: 3,
|
||||
lg: 12,
|
||||
},
|
||||
fieldProps: {
|
||||
md: 9,
|
||||
lg: 12,
|
||||
},
|
||||
};
|
||||
const { renderField, renderInputField, renderDateField, renderURLListField } =
|
||||
formikUtils(intl, formik, splitProps);
|
||||
|
||||
@@ -466,7 +479,13 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
{renderInputField("title")}
|
||||
{renderInputField("code", "text", "scene_code")}
|
||||
|
||||
{renderURLListField("urls", onScrapeGalleryURL, urlScrapable)}
|
||||
{renderURLListField(
|
||||
"urls",
|
||||
onScrapeGalleryURL,
|
||||
urlScrapable,
|
||||
"urls",
|
||||
urlProps
|
||||
)}
|
||||
|
||||
{renderDateField("date")}
|
||||
{renderInputField("photographer")}
|
||||
|
||||
@@ -204,6 +204,10 @@ $galleryTabWidth: 450px;
|
||||
font-size: 1.3em;
|
||||
height: calc(1.5em + 0.75rem + 2px);
|
||||
}
|
||||
|
||||
.form-group[data-field="urls"] .string-list-input input.form-control {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery-cover {
|
||||
|
||||
@@ -320,6 +320,19 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
xl: 12,
|
||||
},
|
||||
};
|
||||
const urlProps = isNew
|
||||
? splitProps
|
||||
: {
|
||||
labelProps: {
|
||||
column: true,
|
||||
md: 3,
|
||||
lg: 12,
|
||||
},
|
||||
fieldProps: {
|
||||
md: 9,
|
||||
lg: 12,
|
||||
},
|
||||
};
|
||||
const { renderField, renderInputField, renderDateField, renderURLListField } =
|
||||
formikUtils(intl, formik, splitProps);
|
||||
|
||||
@@ -461,7 +474,13 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
{renderInputField("title")}
|
||||
{renderInputField("code", "text", "scene_code")}
|
||||
|
||||
{renderURLListField("urls", onScrapeImageURL, urlScrapable)}
|
||||
{renderURLListField(
|
||||
"urls",
|
||||
onScrapeImageURL,
|
||||
urlScrapable,
|
||||
"urls",
|
||||
urlProps
|
||||
)}
|
||||
|
||||
{renderDateField("date")}
|
||||
{renderInputField("photographer")}
|
||||
|
||||
@@ -175,6 +175,10 @@ $imageTabWidth: 450px;
|
||||
font-size: 1.3em;
|
||||
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 {
|
||||
|
||||
@@ -694,7 +694,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
{renderInputField("name")}
|
||||
{renderInputField("disambiguation")}
|
||||
|
||||
{renderStringListField("alias_list", "aliases")}
|
||||
{renderStringListField("alias_list", "aliases", { orderable: false })}
|
||||
|
||||
{renderSelectField("gender", stringGenderMap)}
|
||||
|
||||
|
||||
@@ -602,6 +602,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
xl: 12,
|
||||
},
|
||||
};
|
||||
const urlProps = isNew
|
||||
? splitProps
|
||||
: {
|
||||
labelProps: {
|
||||
column: true,
|
||||
md: 3,
|
||||
lg: 12,
|
||||
},
|
||||
fieldProps: {
|
||||
md: 9,
|
||||
lg: 12,
|
||||
},
|
||||
};
|
||||
const {
|
||||
renderField,
|
||||
renderInputField,
|
||||
@@ -770,7 +783,13 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
{renderInputField("title")}
|
||||
{renderInputField("code", "text", "scene_code")}
|
||||
|
||||
{renderURLListField("urls", onScrapeSceneURL, urlScrapable)}
|
||||
{renderURLListField(
|
||||
"urls",
|
||||
onScrapeSceneURL,
|
||||
urlScrapable,
|
||||
"urls",
|
||||
urlProps
|
||||
)}
|
||||
|
||||
{renderDateField("date")}
|
||||
{renderInputField("director")}
|
||||
|
||||
@@ -558,6 +558,10 @@ input[type="range"].blue-slider {
|
||||
top: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group[data-field="urls"] .string-list-input input.form-control {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
.scene-markers-panel {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { ComponentType } from "react";
|
||||
import { faGripVertical, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { ComponentType, useState } from "react";
|
||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface IStringListInputProps {
|
||||
errors?: string;
|
||||
errorIdx?: number[];
|
||||
readOnly?: boolean;
|
||||
// defaults to true if not set
|
||||
orderable?: boolean;
|
||||
}
|
||||
|
||||
export const StringInput: React.FC<IListInputComponentProps> = ({
|
||||
@@ -51,6 +53,9 @@ export const StringListInput: React.FC<IStringListInputProps> = (props) => {
|
||||
const Input = props.inputComponent ?? StringInput;
|
||||
const AppendComponent = props.appendComponent;
|
||||
const values = props.value.concat("");
|
||||
const [draggedIdx, setDraggedIdx] = useState<number | null>(null);
|
||||
|
||||
const { orderable = true } = props;
|
||||
|
||||
function valueChanged(idx: number, value: string) {
|
||||
const newValues = props.value.slice();
|
||||
@@ -70,12 +75,46 @@ export const StringListInput: React.FC<IStringListInputProps> = (props) => {
|
||||
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 (
|
||||
<>
|
||||
<div className={`string-list-input ${props.errors ? "is-invalid" : ""}`}>
|
||||
<Form.Group>
|
||||
{values.map((v, i) => (
|
||||
<InputGroup className={props.className} key={i}>
|
||||
<InputGroup
|
||||
className={props.className}
|
||||
key={i}
|
||||
onDragOver={(e) => handleDragOver(e, i)}
|
||||
>
|
||||
<Input
|
||||
value={v}
|
||||
setValue={(value) => valueChanged(i, value)}
|
||||
@@ -85,11 +124,24 @@ export const StringListInput: React.FC<IStringListInputProps> = (props) => {
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
{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 && (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => removeValue(i)}
|
||||
disabled={i === values.length - 1}
|
||||
size="sm"
|
||||
>
|
||||
<Icon icon={faMinus} />
|
||||
</Button>
|
||||
|
||||
@@ -412,6 +412,25 @@ button.collapse-button {
|
||||
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 {
|
||||
|
||||
@@ -241,7 +241,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||
<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
|
||||
{renderInputField("name")}
|
||||
{renderInputField("sort_name", "text")}
|
||||
{renderStringListField("aliases")}
|
||||
{renderStringListField("aliases", "aliases", { orderable: false })}
|
||||
{renderInputField("description", "textarea")}
|
||||
{renderParentTagsField()}
|
||||
{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(
|
||||
field: Field,
|
||||
messageID: string = field,
|
||||
props?: IProps
|
||||
props?: IStringListProps
|
||||
) {
|
||||
const value = formik.values[field] as string[];
|
||||
const error = formik.errors[field] as ErrorMessage[] | ErrorMessage;
|
||||
@@ -325,6 +330,7 @@ export function formikUtils<V extends FormikValues>(
|
||||
setValue={(v) => formik.setFieldValue(field, v)}
|
||||
errors={errorMsg}
|
||||
errorIdx={errorIdx}
|
||||
orderable={props?.orderable}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user