diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index 5b9fa9da1..fe7959c55 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -350,6 +350,19 @@ export const GalleryEditPanel: React.FC = ({ 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 = ({ {renderInputField("title")} {renderInputField("code", "text", "scene_code")} - {renderURLListField("urls", onScrapeGalleryURL, urlScrapable)} + {renderURLListField( + "urls", + onScrapeGalleryURL, + urlScrapable, + "urls", + urlProps + )} {renderDateField("date")} {renderInputField("photographer")} diff --git a/ui/v2.5/src/components/Galleries/styles.scss b/ui/v2.5/src/components/Galleries/styles.scss index 5e8618326..9890e887b 100644 --- a/ui/v2.5/src/components/Galleries/styles.scss +++ b/ui/v2.5/src/components/Galleries/styles.scss @@ -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 { diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx index f2771f542..58b809d41 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx @@ -320,6 +320,19 @@ export const ImageEditPanel: React.FC = ({ 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 = ({ {renderInputField("title")} {renderInputField("code", "text", "scene_code")} - {renderURLListField("urls", onScrapeImageURL, urlScrapable)} + {renderURLListField( + "urls", + onScrapeImageURL, + urlScrapable, + "urls", + urlProps + )} {renderDateField("date")} {renderInputField("photographer")} diff --git a/ui/v2.5/src/components/Images/styles.scss b/ui/v2.5/src/components/Images/styles.scss index 936947bc3..8bc736aae 100644 --- a/ui/v2.5/src/components/Images/styles.scss +++ b/ui/v2.5/src/components/Images/styles.scss @@ -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 { diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index 8d1352da0..f2d825e07 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -694,7 +694,7 @@ export const PerformerEditPanel: React.FC = ({ {renderInputField("name")} {renderInputField("disambiguation")} - {renderStringListField("alias_list", "aliases")} + {renderStringListField("alias_list", "aliases", { orderable: false })} {renderSelectField("gender", stringGenderMap)} diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 11575ea7b..9ba5059de 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -602,6 +602,19 @@ export const SceneEditPanel: React.FC = ({ 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 = ({ {renderInputField("title")} {renderInputField("code", "text", "scene_code")} - {renderURLListField("urls", onScrapeSceneURL, urlScrapable)} + {renderURLListField( + "urls", + onScrapeSceneURL, + urlScrapable, + "urls", + urlProps + )} {renderDateField("date")} {renderInputField("director")} diff --git a/ui/v2.5/src/components/Scenes/styles.scss b/ui/v2.5/src/components/Scenes/styles.scss index 92c13d648..78644b4c9 100644 --- a/ui/v2.5/src/components/Scenes/styles.scss +++ b/ui/v2.5/src/components/Scenes/styles.scss @@ -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 { diff --git a/ui/v2.5/src/components/Shared/StringListInput.tsx b/ui/v2.5/src/components/Shared/StringListInput.tsx index 768f282a0..89401884c 100644 --- a/ui/v2.5/src/components/Shared/StringListInput.tsx +++ b/ui/v2.5/src/components/Shared/StringListInput.tsx @@ -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 = ({ @@ -51,6 +53,9 @@ export const StringListInput: React.FC = (props) => { const Input = props.inputComponent ?? StringInput; const AppendComponent = props.appendComponent; const values = props.value.concat(""); + const [draggedIdx, setDraggedIdx] = useState(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 = (props) => { props.setValue(newValues); } + function handleDragStart(event: React.DragEvent, 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 ( <>
{values.map((v, i) => ( - + handleDragOver(e, i)} + > valueChanged(i, value)} @@ -85,11 +124,24 @@ export const StringListInput: React.FC = (props) => { /> {AppendComponent && } + {!props.readOnly && values.length > 2 && orderable && ( + + )} {!props.readOnly && ( diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index ce8454b89..aed03cef9 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -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 { diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx index e734de846..a067470a9 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx @@ -241,7 +241,7 @@ export const TagEditPanel: React.FC = ({
{renderInputField("name")} {renderInputField("sort_name", "text")} - {renderStringListField("aliases")} + {renderStringListField("aliases", "aliases", { orderable: false })} {renderInputField("description", "textarea")} {renderParentTagsField()} {renderSubTagsField()} diff --git a/ui/v2.5/src/utils/form.tsx b/ui/v2.5/src/utils/form.tsx index 9798efd19..fbf239a9b 100644 --- a/ui/v2.5/src/utils/form.tsx +++ b/ui/v2.5/src/utils/form.tsx @@ -308,10 +308,15 @@ export function formikUtils( } } + 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( setValue={(v) => formik.setFieldValue(field, v)} errors={errorMsg} errorIdx={errorIdx} + orderable={props?.orderable} /> );