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:
WithoutPants
2025-12-11 13:07:05 +11:00
committed by GitHub
parent 0980daa99e
commit 11417590ee
11 changed files with 155 additions and 9 deletions

View File

@@ -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")}

View File

@@ -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 {

View File

@@ -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")}

View File

@@ -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 {

View File

@@ -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)}

View File

@@ -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")}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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()}

View File

@@ -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}
/>
);