This commit is contained in:
Infinite
2020-01-29 22:28:13 +01:00
parent 3f9f32c356
commit c1ce6d539d
45 changed files with 419 additions and 310 deletions

View File

@@ -1,7 +1,7 @@
import React from "react";
import { useParams } from "react-router-dom";
import { StashService } from "src/core/StashService";
import { LoadingIndicator } from 'src/components/Shared';
import { LoadingIndicator } from "src/components/Shared";
import { GalleryViewer } from "./GalleryViewer";
export const Gallery: React.FC = () => {
@@ -10,8 +10,7 @@ export const Gallery: React.FC = () => {
const { data, error, loading } = StashService.useFindGallery(id);
const gallery = data?.findGallery;
if (loading || !gallery)
return <LoadingIndicator />;
if (loading || !gallery) return <LoadingIndicator />;
if (error) return <div>{error.message}</div>;
return (

View File

@@ -91,9 +91,7 @@ export const MainNavbar: React.FC = () => {
))}
</Nav>
<Nav>
<div className="d-none d-sm-block">
{newButton}
</div>
<div className="d-none d-sm-block">{newButton}</div>
<LinkContainer exact to="/settings">
<Button className="minimal">
<Icon icon="cog" />

View File

@@ -1,4 +1,3 @@
export class ParserField {
public field: string;
public helperText?: string;

View File

@@ -1,13 +1,13 @@
import React, { useState } from 'react';
import React, { useState } from "react";
import {
Button,
Dropdown,
DropdownButton,
Form,
InputGroup,
} from 'react-bootstrap';
import { ParserField } from './ParserField';
import { ShowFields } from './ShowFields';
InputGroup
} from "react-bootstrap";
import { ParserField } from "./ParserField";
import { ShowFields } from "./ShowFields";
const builtInRecipes = [
{
@@ -80,7 +80,9 @@ interface IParserInputProps {
setShowFields: (fields: Map<string, boolean>) => void;
}
export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProps) => {
export const ParserInput: React.FC<IParserInputProps> = (
props: IParserInputProps
) => {
const [pattern, setPattern] = useState<string>(props.input.pattern);
const [ignoreWords, setIgnoreWords] = useState<string>(
props.input.ignoreWords.join(" ")
@@ -124,7 +126,9 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
return (
<Form.Group>
<Form.Group className="row">
<Form.Label htmlFor="filename-pattern" className="col-2">Filename Pattern</Form.Label>
<Form.Label htmlFor="filename-pattern" className="col-2">
Filename Pattern
</Form.Label>
<InputGroup className="col-8">
<Form.Control
id="filename-pattern"
@@ -134,7 +138,10 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
<InputGroup.Append>
<DropdownButton id="parser-field-select" title="Add Field">
{validFields.map(item => (
<Dropdown.Item key={item.field} onSelect={() => addParserField(item)}>
<Dropdown.Item
key={item.field}
onSelect={() => addParserField(item)}
>
<span>{item.field}</span>
<span className="ml-auto">{item.helperText}</span>
</Dropdown.Item>
@@ -142,7 +149,9 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
</DropdownButton>
</InputGroup.Append>
</InputGroup>
<Form.Text className="text-muted row col-10 offset-2">Use &apos;\\&apos; to escape literal {} characters</Form.Text>
<Form.Text className="text-muted row col-10 offset-2">
Use &apos;\\&apos; to escape literal {} characters
</Form.Text>
</Form.Group>
<Form.Group className="row" controlId="ignored-words">
@@ -152,12 +161,16 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
onChange={(newValue: any) => setIgnoreWords(newValue.target.value)}
value={ignoreWords}
/>
<Form.Text className="text-muted col-10 offset-2">Matches with {"{i}"}</Form.Text>
<Form.Text className="text-muted col-10 offset-2">
Matches with {"{i}"}
</Form.Text>
</Form.Group>
<h5>Title</h5>
<Form.Group className="row">
<Form.Label htmlFor="whitespace-characters" className="col-2">Whitespace characters:</Form.Label>
<Form.Label htmlFor="whitespace-characters" className="col-2">
Whitespace characters:
</Form.Label>
<Form.Control
className="col-8"
onChange={(newValue: any) =>
@@ -170,7 +183,9 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
</Form.Text>
</Form.Group>
<Form.Group className="row">
<Form.Label htmlFor="capitalize-title" className="col-2">Capitalize title</Form.Label>
<Form.Label htmlFor="capitalize-title" className="col-2">
Capitalize title
</Form.Label>
<Form.Control
className="col-8"
type="checkbox"
@@ -182,9 +197,16 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
{/* TODO - mapping stuff will go here */}
<Form.Group>
<DropdownButton variant="secondary" id="recipe-select" title="Select Parser Recipe">
<DropdownButton
variant="secondary"
id="recipe-select"
title="Select Parser Recipe"
>
{builtInRecipes.map(item => (
<Dropdown.Item key={item.pattern} onSelect={() => setParserRecipe(item)}>
<Dropdown.Item
key={item.pattern}
onSelect={() => setParserRecipe(item)}
>
<span>{item.pattern}</span>
<span className="mr-auto">{item.description}</span>
</Dropdown.Item>
@@ -200,7 +222,9 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
</Form.Group>
<Form.Group className="row">
<Button variant="secondary" className="col-1" onClick={onFind}>Find</Button>
<Button variant="secondary" className="col-1" onClick={onFind}>
Find
</Button>
<Form.Control
as="select"
style={{ flexBasis: "min-content" }}
@@ -218,4 +242,4 @@ export const ParserInput: React.FC<IParserInputProps> = (props: IParserInputProp
</Form.Group>
</Form.Group>
);
}
};

View File

@@ -1,22 +1,20 @@
/* eslint-disable no-param-reassign, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
import React, { useEffect, useState, useCallback } from "react";
import {
Badge,
Button,
Card,
Form,
Table
} from "react-bootstrap";
import { Badge, Button, Card, Form, Table } from "react-bootstrap";
import _ from "lodash";
import { StashService } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { FilterSelect, StudioSelect, LoadingIndicator } from "src/components/Shared";
import {
FilterSelect,
StudioSelect,
LoadingIndicator
} from "src/components/Shared";
import { TextUtils } from "src/utils";
import { useToast } from "src/hooks";
import { Pagination } from "../list/Pagination";
import { IParserInput, ParserInput } from './ParserInput';
import { ParserField } from './ParserField';
import { IParserInput, ParserInput } from "./ParserInput";
import { ParserField } from "./ParserField";
class ParserResult<T> {
public value: GQL.Maybe<T> = null;

View File

@@ -1,9 +1,6 @@
import React, { useState } from 'react';
import {
Button,
Collapse
} from 'react-bootstrap';
import { Icon } from 'src/components/Shared';
import React, { useState } from "react";
import { Button, Collapse } from "react-bootstrap";
import { Icon } from "src/components/Shared";
interface IShowFieldsProps {
fields: Map<string, boolean>;
@@ -43,4 +40,4 @@ export const ShowFields = (props: IShowFieldsProps) => {
</Collapse>
</div>
);
}
};

View File

@@ -1,6 +1,6 @@
import React from "react";
import { Button, Table } from "react-bootstrap";
import { LoadingIndicator } from 'src/components/Shared';
import { LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService";
export const SettingsAboutPanel: React.FC = () => {

View File

@@ -153,8 +153,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
}
if (error) return <h1>{error.message}</h1>;
if (!data?.configuration || loading)
return <LoadingIndicator />;
if (!data?.configuration || loading) return <LoadingIndicator />;
return (
<>
@@ -199,8 +198,8 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group>
<h6>Excluded Patterns</h6>
<Form.Group>
{excludes
&& excludes.map((regexp, i) => (
{excludes &&
excludes.map((regexp, i) => (
<InputGroup>
<Form.Control
className="col col-sm-6"
@@ -218,8 +217,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
</Button>
</InputGroup.Append>
</InputGroup>
))
}
))}
</Form.Group>
<Button className="minimal" onClick={() => excludeAddRegex()}>
<Icon icon="plus" />

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { DurationInput, LoadingIndicator } from 'src/components/Shared';
import { DurationInput, LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService";
import { useToast } from "src/hooks";
@@ -108,7 +108,8 @@ export const SettingsInterfacePanel: React.FC = () => {
onValueChange={duration => setMaximumLoopDuration(duration)}
/>
<Form.Text className="text-muted">
Maximum scene duration where scene player will loop the video - 0 to disable
Maximum scene duration where scene player will loop the video - 0 to
disable
</Form.Text>
</Form.Group>
</Form.Group>

View File

@@ -53,7 +53,12 @@ export const GenerateButton: React.FC = () => {
/>
</Form.Group>
<Form.Group>
<Button id="generate" variant="secondary" type="submit" onClick={() => onGenerate()}>
<Button
id="generate"
variant="secondary"
type="submit"
onClick={() => onGenerate()}
>
Generate
</Button>
<Form.Text className="text-muted">

View File

@@ -242,9 +242,7 @@ export const SettingsTasksPanel: React.FC = () => {
<Form.Group>
<Link to="/sceneFilenameParser">
<Button variant="secondary">
Scene Filename Parser
</Button>
<Button variant="secondary">Scene Filename Parser</Button>
</Link>
</Form.Group>

View File

@@ -1,10 +1,7 @@
import {
Button,
Modal,
} from "react-bootstrap";
import { Button, Modal } from "react-bootstrap";
import React, { useState } from "react";
import * as GQL from "src/core/generated-graphql";
import { ImageInput } from 'src/components/Shared';
import { ImageInput } from "src/components/Shared";
interface IProps {
performer?: Partial<GQL.PerformerDataFragment>;
@@ -24,7 +21,11 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
function renderEditButton() {
if (props.isNew) return;
return (
<Button variant="primary" className="edit" onClick={() => props.onToggleEdit()}>
<Button
variant="primary"
className="edit"
onClick={() => props.onToggleEdit()}
>
{props.isEditing ? "Cancel" : "Edit"}
</Button>
);
@@ -43,7 +44,11 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
function renderDeleteButton() {
if (props.isNew || props.isEditing) return;
return (
<Button variant="danger" className="delete d-none d-sm-block" onClick={() => setIsDeleteAlertOpen(true)}>
<Button
variant="danger"
className="delete d-none d-sm-block"
onClick={() => setIsDeleteAlertOpen(true)}
>
Delete
</Button>
);
@@ -92,7 +97,10 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
return (
<div className="details-edit">
{renderEditButton()}
<ImageInput isEditing={props.isEditing} onImageChange={props.onImageChange} />
<ImageInput
isEditing={props.isEditing}
onImageChange={props.onImageChange}
/>
{renderAutoTagButton()}
{renderSaveButton()}
{renderDeleteButton()}

View File

@@ -35,10 +35,20 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
function renderButtons() {
return (
<ButtonGroup vertical>
<Button variant="secondary" className="duration-button" disabled={props.disabled} onClick={() => increment()}>
<Button
variant="secondary"
className="duration-button"
disabled={props.disabled}
onClick={() => increment()}
>
<Icon icon="chevron-up" />
</Button>
<Button variant="secondary" className="duration-button" disabled={props.disabled} onClick={() => decrement()}>
<Button
variant="secondary"
className="duration-button"
disabled={props.disabled}
onClick={() => decrement()}
>
<Icon icon="chevron-down" />
</Button>
</ButtonGroup>

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Button, InputGroup, Form, Modal } from "react-bootstrap";
import { LoadingIndicator } from 'src/components/Shared';
import { LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService";
interface IProps {
@@ -80,7 +80,9 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="success" onClick={() => onSelectDirectory()}>Add</Button>
<Button variant="success" onClick={() => onSelectDirectory()}>
Add
</Button>
</Modal.Footer>
</Modal>
);
@@ -103,7 +105,9 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
})}
</Form.Group>
<Button variant="secondary" onClick={() => setIsDisplayingDialog(true)}>Add Directory</Button>
<Button variant="secondary" onClick={() => setIsDisplayingDialog(true)}>
Add Directory
</Button>
</>
);
};

View File

@@ -1,12 +1,15 @@
import React from "react";
import { Button, Form } from 'react-bootstrap';
import { Button, Form } from "react-bootstrap";
interface IImageInput {
isEditing: boolean;
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
}
export const ImageInput: React.FC<IImageInput> = ({ isEditing, onImageChange }) => {
export const ImageInput: React.FC<IImageInput> = ({
isEditing,
onImageChange
}) => {
if (!isEditing) return <div />;
return (
@@ -19,5 +22,4 @@ export const ImageInput: React.FC<IImageInput> = ({ isEditing, onImageChange })
/>
</Form.Label>
);
}
};

View File

@@ -1,6 +1,6 @@
import React from "react";
import { Spinner } from "react-bootstrap";
import cx from 'classnames';
import cx from "classnames";
interface ILoadingProps {
message?: string;
@@ -10,7 +10,10 @@ interface ILoadingProps {
const CLASSNAME = "LoadingIndicator";
const CLASSNAME_MESSAGE = `${CLASSNAME}-message`;
const LoadingIndicator: React.FC<ILoadingProps> = ({ message, inline = false }) => (
const LoadingIndicator: React.FC<ILoadingProps> = ({
message,
inline = false
}) => (
<div className={cx(CLASSNAME, { inline })}>
<Spinner animation="border" role="status">
<span className="sr-only">Loading...</span>

View File

@@ -35,7 +35,7 @@ interface ISelectProps {
onChange: (item: ValueType<Option>) => void;
initialIds?: string[];
isMulti?: boolean;
isClearable?: boolean,
isClearable?: boolean;
onInputChange?: (input: string) => void;
placeholder?: string;
showDropdown?: boolean;
@@ -53,10 +53,11 @@ interface ISceneGallerySelect {
}
const getSelectedValues = (selectedItems: ValueType<Option>) =>
selectedItems ?
(Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map(
selectedItems
? (Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map(
item => item.value
) : [];
)
: [];
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => {
const { data, loading } = StashService.useValidGalleriesForScene(
@@ -176,8 +177,9 @@ export const PerformerSelect: React.FC<IFilterProps> = props => {
label: item.name ?? ""
}));
const placeholder = props.noSelectionString ?? "Select performer...";
const selectedOptions:Option[] = props.ids ?
items.filter(item => props.ids?.indexOf(item.value) !== -1) : [];
const selectedOptions: Option[] = props.ids
? items.filter(item => props.ids?.indexOf(item.value) !== -1)
: [];
const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems);
@@ -208,8 +210,9 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
label: item.name
}));
const placeholder = props.noSelectionString ?? "Select studio...";
const selectedOptions:Option[] = props.ids ?
items.filter(item => props.ids?.indexOf(item.value) !== -1) : [];
const selectedOptions: Option[] = props.ids
? items.filter(item => props.ids?.indexOf(item.value) !== -1)
: [];
const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems);
@@ -320,10 +323,14 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
const defaultValue =
items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
const options = groupHeader ? [{
const options = groupHeader
? [
{
label: groupHeader,
options: items
}] : items;
}
]
: items;
const styles = {
option: (base: CSSProperties) => ({

View File

@@ -15,4 +15,4 @@ export { DurationInput } from "./DurationInput";
export { TagLink } from "./TagLink";
export { HoverPopover } from "./HoverPopover";
export { default as LoadingIndicator } from "./LoadingIndicator";
export { ImageInput } from './ImageInput';
export { ImageInput } from "./ImageInput";

View File

@@ -46,7 +46,10 @@ export const Stats: React.FC = () => {
</nav>
<h5>Notes</h5>
<em>This is still an early version, some things are still a work in progress.</em>
<em>
This is still an early version, some things are still a work in
progress.
</em>
</div>
</div>
);

View File

@@ -10,11 +10,8 @@ interface IProps {
export const StudioCard: React.FC<IProps> = ({ studio }) => {
return (
<Card className="studio-card">
<Link
to={`/studios/${studio.id}`}
className="studio-image"
>
<img alt={studio.name} src={studio.image_path ?? ''} />
<Link to={`/studios/${studio.id}`} className="studio-image">
<img alt={studio.name} src={studio.image_path ?? ""} />
</Link>
<div className="card-section">
<h5 className="text-truncate">{studio.name}</h5>

View File

@@ -3,14 +3,18 @@
import { Table } from "react-bootstrap";
import React, { useEffect, useState } from "react";
import { useParams, useHistory } from "react-router-dom";
import cx from 'classnames';
import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { ImageUtils, TableUtils } from "src/utils";
import { DetailsEditNavbar, Modal, LoadingIndicator } from "src/components/Shared";
import {
DetailsEditNavbar,
Modal,
LoadingIndicator
} from "src/components/Shared";
import { useToast } from "src/hooks";
import { StudioScenesPanel } from './StudioScenesPanel';
import { StudioScenesPanel } from "./StudioScenesPanel";
export const Studio: React.FC = () => {
const history = useHistory();
@@ -71,8 +75,7 @@ export const Studio: React.FC = () => {
ImageUtils.usePasteImage(onImageLoad);
if (!isNew && !isEditing) {
if (!data?.findStudio || loading)
return <LoadingIndicator />;
if (!data?.findStudio || loading) return <LoadingIndicator />;
if (error) return <div>{error.message}</div>;
}
@@ -142,21 +145,26 @@ export const Studio: React.FC = () => {
accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
>
<p>Are you sure you want to delete {studio.name ?? 'studio'}?</p>
<p>Are you sure you want to delete {studio.name ?? "studio"}?</p>
</Modal>
);
}
return (
<div className="row">
<div className={cx('studio-details', { 'col ml-sm-5': !isNew, 'col-8': isNew})}>
<div
className={cx("studio-details", {
"col ml-sm-5": !isNew,
"col-8": isNew
})}
>
{isNew && <h2>Add Studio</h2>}
<img className="logo" alt={name} src={imagePreview} />
<Table id="performer-details" style={{ width: "100%" }}>
<tbody>
{TableUtils.renderInputGroup({
title: "Name",
value: studio.name ?? '',
value: studio.name ?? "",
isEditing: !!isEditing,
onChange: setName
})}

View File

@@ -8,9 +8,7 @@ interface IStudioScenesPanel {
studio: Partial<GQL.StudioDataFragment>;
}
export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({
studio
}) => {
export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ studio }) => {
function filterHook(filter: ListFilterModel) {
const studioValue = { id: studio.id!, label: studio.name! };
// if studio is already present, then we modify it, otherwise add

View File

@@ -14,8 +14,7 @@ export const StudioList: React.FC = () => {
result: FindStudiosQueryResult,
filter: ListFilterModel
) {
if (!result.data?.findStudios)
return;
if (!result.data?.findStudios) return;
if (filter.displayMode === DisplayMode.Grid) {
return (

View File

@@ -93,8 +93,7 @@ export const TagList: React.FC = () => {
</Modal>
);
if (!data?.allTags)
return <LoadingIndicator />;
if (!data?.allTags) return <LoadingIndicator />;
if (error) return <div>{error.message}</div>;
const tagElements = data.allTags.map(tag => {
@@ -104,7 +103,9 @@ export const TagList: React.FC = () => {
{tag.name}
</Button>
<div style={{ float: "right" }}>
<Button variant="secondary" onClick={() => onAutoTag(tag)}>Auto Tag</Button>
<Button variant="secondary" onClick={() => onAutoTag(tag)}>
Auto Tag
</Button>
<Button variant="secondary">
<Link to={NavUtils.makeTagScenesUrl(tag)}>
Scenes: {tag.scene_count}

View File

@@ -16,9 +16,7 @@ interface IWallItemProps {
) => void;
}
export const WallItem: React.FC<IWallItemProps> = (
props: IWallItemProps
) => {
export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
const [videoPath, setVideoPath] = useState<string>();
const [previewPath, setPreviewPath] = useState<string>("");
const [screenshotPath, setScreenshotPath] = useState<string>("");
@@ -28,7 +26,8 @@ export const WallItem: React.FC<IWallItemProps> = (
const videoHoverHook = VideoHoverHook.useVideoHover({
resetOnMouseLeave: true
});
const showTextContainer = config.data?.configuration.interface.wallShowTitle ?? true;
const showTextContainer =
config.data?.configuration.interface.wallShowTitle ?? true;
function onMouseEnter() {
VideoHoverHook.onMouseEnter(videoHoverHook);

View File

@@ -123,7 +123,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
value={criterion.modifier}
>
{criterion.modifierOptions.map(c => (
<option key={c.value} value={c.value}>{c.label}</option>
<option key={c.value} value={c.value}>
{c.label}
</option>
))}
</Form.Control>
);
@@ -156,7 +158,10 @@ export const AddFilter: React.FC<IAddFilterProps> = (
isMulti
onSelect={items => {
const newCriterion = _.cloneDeep(criterion);
newCriterion.value = items.map(i => ({ id: i.id, label: i.name! }));
newCriterion.value = items.map(i => ({
id: i.id,
label: i.name!
}));
setCriterion(newCriterion);
}}
ids={criterion.value.map((labeled: any) => labeled.id)}
@@ -172,7 +177,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
value={criterion.value}
>
{criterion.options.map(c => (
<option key={c} value={c}>{c}</option>
<option key={c} value={c}>
{c}
</option>
))}
</Form.Control>
);
@@ -216,7 +223,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
value={criterion.type}
>
{props.filter.criterionOptions.map(c => (
<option key={c.value} value={c.value}>{c.label}</option>
<option key={c.value} value={c.value}>
{c.label}
</option>
))}
</Form.Control>
</Form.Group>

View File

@@ -158,7 +158,10 @@ export const ListFilter: React.FC<IListFilterProps> = (
onClick={() => onClickCriterionTag(criterion)}
>
{criterion.getLabel()}
<Button variant="secondary" onClick={() => onRemoveCriterionTag(criterion)}>
<Button
variant="secondary"
onClick={() => onRemoveCriterionTag(criterion)}
>
<Icon icon="times" />
</Button>
</Badge>
@@ -180,7 +183,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
function renderSelectAll() {
if (props.onSelectAll) {
return (
<Dropdown.Item key="select-all" onClick={() => onSelectAll()}>Select All</Dropdown.Item>
<Dropdown.Item key="select-all" onClick={() => onSelectAll()}>
Select All
</Dropdown.Item>
);
}
}
@@ -201,7 +206,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
if (props.otherOperations) {
props.otherOperations.forEach(o => {
options.push(
<Dropdown.Item key={o.text} onClick={o.onClick}>{o.text}</Dropdown.Item>
<Dropdown.Item key={o.text} onClick={o.onClick}>
{o.text}
</Dropdown.Item>
);
});
}
@@ -259,7 +266,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
className="filter-item col-1 d-none d-sm-inline"
>
{PAGE_SIZE_OPTIONS.map(s => (
<option value={s} key={s}>{s}</option>
<option value={s} key={s}>
{s}
</option>
))}
</Form.Control>
<ButtonGroup className="filter-item">
@@ -288,7 +297,6 @@ export const ListFilter: React.FC<IListFilterProps> = (
</Button>
</OverlayTrigger>
</Dropdown>
</ButtonGroup>
<AddFilter
@@ -304,7 +312,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
{maybeRenderZoom()}
<ButtonGroup className="filter-item d-none d-sm-inline-flex">{renderMore()}</ButtonGroup>
<ButtonGroup className="filter-item d-none d-sm-inline-flex">
{renderMore()}
</ButtonGroup>
</div>
<div
style={{

View File

@@ -38,14 +38,14 @@ export const Pagination: React.FC<IPaginationProps> = ({
);
const calculatePageClass = (buttonPage: number) => {
if(pages.length <= 4) return '';
if (pages.length <= 4) return "";
if(currentPage === 1 && buttonPage <= 4) return '';
if (currentPage === 1 && buttonPage <= 4) return "";
const maxPage = pages[pages.length - 1];
if(currentPage === maxPage && buttonPage > (maxPage - 3)) return '';
if(Math.abs(buttonPage - currentPage) <= 1) return '';
return 'd-none d-sm-block'
}
if (currentPage === maxPage && buttonPage > maxPage - 3) return "";
if (Math.abs(buttonPage - currentPage) <= 1) return "";
return "d-none d-sm-block";
};
const pageButtons = pages.map((page: number) => (
<Button
@@ -59,12 +59,15 @@ export const Pagination: React.FC<IPaginationProps> = ({
</Button>
));
if(pages.length <= 1)
return <div />;
if (pages.length <= 1) return <div />;
return (
<ButtonGroup className="filter-container pagination">
<Button variant="secondary" disabled={currentPage === 1} onClick={() => onChangePage(1)}>
<Button
variant="secondary"
disabled={currentPage === 1}
onClick={() => onChangePage(1)}
>
<span className="d-none d-sm-inline">First</span>
<span className="d-inline d-sm-none">&#x300a;</span>
</Button>

View File

@@ -3,7 +3,7 @@
import React, { useEffect, useState } from "react";
import { Button, Tabs, Tab } from "react-bootstrap";
import { useParams, useHistory } from "react-router-dom";
import cx from 'classnames'
import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { Icon, LoadingIndicator } from "src/components/Shared";
@@ -166,34 +166,52 @@ export const Performer: React.FC = () => {
const renderIcons = () => (
<span className="name-icons d-block d-sm-inline">
<Button
className={cx('minimal', performer.favorite ? "favorite" : "not-favorite")}
className={cx(
"minimal",
performer.favorite ? "favorite" : "not-favorite"
)}
onClick={() => setFavorite(!performer.favorite)}
>
<Icon icon="heart" />
</Button>
{performer.url && (
<Button className="minimal">
<a href={performer.url} className="link" target="_blank" rel="noopener noreferrer">
<a
href={performer.url}
className="link"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="link" />
</a>
</Button>
)}
{performer.twitter && (
<Button className="minimal">
<a href={`https://www.twitter.com/${performer.twitter}`} className="twitter" target="_blank" rel="noopener noreferrer">
<a
href={`https://www.twitter.com/${performer.twitter}`}
className="twitter"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="dove" />
</a>
</Button>
)}
{performer.instagram && (
<Button className="minimal">
<a href={`https://www.instagram.com/${performer.instagram}`} className="instagram" target="_blank" rel="noopener noreferrer">
<a
href={`https://www.instagram.com/${performer.instagram}`}
className="instagram"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="camera" />
</a>
</Button>
)}
</span>
)
);
function renderNewView() {
return (

View File

@@ -1,16 +1,16 @@
/* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react";
import {
Button,
Form,
Popover,
OverlayTrigger,
Table
} from "react-bootstrap";
import { Button, Form, Popover, OverlayTrigger, Table } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { Icon, Modal, ImageInput, ScrapePerformerSuggest, LoadingIndicator } from "src/components/Shared";
import {
Icon,
Modal,
ImageInput,
ScrapePerformerSuggest,
LoadingIndicator
} from "src/components/Shared";
import { ImageUtils, TableUtils } from "src/utils";
import { useToast } from "src/hooks";
@@ -276,7 +276,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
return undefined;
}
return (
<Button className="minimal scrape-url-button" onClick={() => onScrapePerformerURL()}>
<Button
className="minimal scrape-url-button"
onClick={() => onScrapePerformerURL()}
>
<Icon icon="file-upload" />
</Button>
);
@@ -446,7 +449,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
isEditing: !!isEditing,
onChange: setInstagram
})}
<ImageInput isEditing={!!isEditing} onImageChange={onImageChangeHandler} />
<ImageInput
isEditing={!!isEditing}
onImageChange={onImageChangeHandler}
/>
</tbody>
</Table>

View File

@@ -1,6 +1,6 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { Button, Badge, Card } from 'react-bootstrap';
import { Button, Badge, Card } from "react-bootstrap";
import { TextUtils } from "src/utils";
interface IPrimaryTags {
@@ -9,14 +9,17 @@ interface IPrimaryTags {
onEdit: (marker: GQL.SceneMarkerDataFragment) => void;
}
export const PrimaryTags: React.FC<IPrimaryTags> = ({ sceneMarkers, onClickMarker, onEdit }) => {
export const PrimaryTags: React.FC<IPrimaryTags> = ({
sceneMarkers,
onClickMarker,
onEdit
}) => {
if (!sceneMarkers?.length) return <div />;
const primaries: Record<string, GQL.Tag> = {};
const primaryTags: Record<string, GQL.SceneMarkerDataFragment[]> = {};
sceneMarkers.forEach(m => {
if(primaryTags[m.primary_tag.id])
primaryTags[m.primary_tag.id].push(m);
if (primaryTags[m.primary_tag.id]) primaryTags[m.primary_tag.id].push(m);
else {
primaryTags[m.primary_tag.id] = [m];
primaries[m.primary_tag.id] = m.primary_tag;
@@ -55,16 +58,10 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({ sceneMarkers, onClickMarke
return (
<Card className="primary-card col-12 col-sm-3" key={id}>
<h3>{primaries[id].name}</h3>
<Card.Body className="primary-card-body">
{ markers }
</Card.Body>
<Card.Body className="primary-card-body">{markers}</Card.Body>
</Card>
);
});
return (
<div className="primary-tag row">
{ primaryCards }
</div>
);
return <div className="primary-tag row">{primaryCards}</div>;
};

View File

@@ -5,7 +5,7 @@ import { useParams, useLocation, useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
import { LoadingIndicator } from 'src/components/Shared';
import { LoadingIndicator } from "src/components/Shared";
import { ScenePlayer } from "src/components/scenes/ScenePlayer/ScenePlayer";
import { ScenePerformerPanel } from "./ScenePerformerPanel";
import { SceneMarkersPanel } from "./SceneMarkersPanel";
@@ -25,8 +25,7 @@ export const Scene: React.FC = () => {
const autoplay = queryParams?.autoplay === "true";
useEffect(() => {
if(data?.findScene)
setScene(data.findScene)
if (data?.findScene) setScene(data.findScene);
}, [data]);
function getInitialTimestamp() {
@@ -50,21 +49,14 @@ export const Scene: React.FC = () => {
return (
<>
<ScenePlayer
scene={scene}
timestamp={timestamp}
autoplay={autoplay}
/>
<ScenePlayer scene={scene} timestamp={timestamp} autoplay={autoplay} />
<div id="details-container" className="col col-sm-9 m-sm-auto">
<Tabs id="scene-tabs" mountOnEnter>
<Tab eventKey="scene-details-panel" title="Details">
<SceneDetailPanel scene={scene} />
</Tab>
<Tab eventKey="scene-markers-panel" title="Markers">
<SceneMarkersPanel
scene={scene}
onClickMarker={onClickMarker}
/>
<SceneMarkersPanel scene={scene} onClickMarker={onClickMarker} />
</Tab>
{scene.performers.length > 0 ? (
<Tab eventKey="scene-performer-panel" title="Performers">
@@ -80,10 +72,18 @@ export const Scene: React.FC = () => {
) : (
""
)}
<Tab className="file-info-panel" eventKey="scene-file-info-panel" title="File Info">
<Tab
className="file-info-panel"
eventKey="scene-file-info-panel"
title="File Info"
>
<SceneFileInfoPanel scene={scene} />
</Tab>
<Tab eventKey="scene-edit-panel" title="Edit" tabClassName="d-none d-sm-block">
<Tab
eventKey="scene-edit-panel"
title="Edit"
tabClassName="d-none d-sm-block"
>
<SceneEditPanel
scene={scene}
onUpdate={newScene => setScene(newScene)}

View File

@@ -38,7 +38,7 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
{props.scene.title ?? TextUtils.fileNameFromPath(props.scene.path)}
</h3>
<div className="col-6 scene-details">
<h4>{props.scene.date ?? ''}</h4>
<h4>{props.scene.date ?? ""}</h4>
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""}
{props.scene.file.height && (
<h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6>
@@ -48,8 +48,14 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
</div>
<div className="col-4 offset-2">
{props.scene.studio && (
<Link className="studio-logo" to={`/studios/${props.scene.studio.id}`}>
<img src={props.scene.studio.image_path ?? ''} alt={`${props.scene.studio.name} logo`} />
<Link
className="studio-logo"
to={`/studios/${props.scene.studio.id}`}
>
<img
src={props.scene.studio.image_path ?? ""}
alt={`${props.scene.studio.name} logo`}
/>
</Link>
)}
</div>

View File

@@ -1,13 +1,7 @@
/* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react";
import {
Button,
Dropdown,
DropdownButton,
Form,
Table
} from "react-bootstrap";
import { Button, Dropdown, DropdownButton, Form, Table } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import {
@@ -294,8 +288,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
);
}
if(isLoading)
return <LoadingIndicator />;
if (isLoading) return <LoadingIndicator />;
return (
<div className="form-container row">
@@ -329,8 +322,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
title: "Rating",
value: rating,
isEditing: true,
onChange: (value: string) => setRating(Number.parseInt(value, 10)),
selectOptions: ['', 1, 2, 3, 4, 5]
onChange: (value: string) =>
setRating(Number.parseInt(value, 10)),
selectOptions: ["", 1, 2, 3, 4, 5]
})}
<tr>
<td>Gallery</td>
@@ -356,7 +350,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<td>
<PerformerSelect
isMulti
onSelect={items => setPerformerIds(items.map(item => item.id))}
onSelect={items =>
setPerformerIds(items.map(item => item.id))
}
ids={performerIds}
/>
</td>

View File

@@ -94,7 +94,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Frame Rate</span>
<span className="col-8 text-truncate">{props.scene.file.framerate} frames per second</span>
<span className="col-8 text-truncate">
{props.scene.file.framerate} frames per second
</span>
</div>
);
}
@@ -106,7 +108,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Bit Rate</span>
<span className="col-8 text-truncate">{TextUtils.bitRate(props.scene.file.bitrate ?? 0)}</span>
<span className="col-8 text-truncate">
{TextUtils.bitRate(props.scene.file.bitrate ?? 0)}
</span>
</div>
);
}
@@ -118,7 +122,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Video Codec</span>
<span className="col-8 text-truncate">{props.scene.file.video_codec}</span>
<span className="col-8 text-truncate">
{props.scene.file.video_codec}
</span>
</div>
);
}
@@ -130,7 +136,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Audio Codec</span>
<span className="col-8 text-truncate">{props.scene.file.audio_codec}</span>
<span className="col-8 text-truncate">
{props.scene.file.audio_codec}
</span>
</div>
);
}

View File

@@ -1,8 +1,5 @@
import React from "react";
import {
Button,
Form
} from "react-bootstrap";
import { Button, Form } from "react-bootstrap";
import { Field, FieldProps, Form as FormikForm, Formik } from "formik";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
@@ -27,16 +24,19 @@ interface ISceneMarkerForm {
onClose: () => void;
}
export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMarker, playerPosition, onClose }) => {
export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
sceneID,
editingMarker,
playerPosition,
onClose
}) => {
const [sceneMarkerCreate] = StashService.useSceneMarkerCreate();
const [sceneMarkerUpdate] = StashService.useSceneMarkerUpdate();
const [sceneMarkerDestroy] = StashService.useSceneMarkerDestroy();
const Toast = useToast();
const onSubmit = (values: IFormFields) => {
const variables:
| GQL.SceneMarkerUpdateInput
| GQL.SceneMarkerCreateInput = {
const variables: GQL.SceneMarkerUpdateInput | GQL.SceneMarkerCreateInput = {
title: values.title,
seconds: parseFloat(values.seconds),
scene_id: sceneID,
@@ -54,7 +54,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
.then(onClose)
.catch(err => Toast.error(err));
}
}
};
const onDelete = () => {
if (!editingMarker) return;
@@ -62,7 +62,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
sceneMarkerDestroy({ variables: { id: editingMarker.id } })
.then(onClose)
.catch(err => Toast.error(err));
}
};
const renderTitleField = (fieldProps: FieldProps<string>) => (
<div className="col-10">
<MarkerTitleSuggest
@@ -84,7 +84,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
Math.round(playerPosition ?? 0)
)
}
numericValue={Number.parseInt(fieldProps.field.value ?? '0', 10)}
numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)}
/>
</div>
);
@@ -114,47 +114,42 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
);
const values: IFormFields = {
title: editingMarker?.title ?? '',
seconds: (editingMarker?.seconds ?? Math.round(playerPosition ?? 0)).toString(),
primaryTagId: editingMarker?.primary_tag.id ?? '',
title: editingMarker?.title ?? "",
seconds: (
editingMarker?.seconds ?? Math.round(playerPosition ?? 0)
).toString(),
primaryTagId: editingMarker?.primary_tag.id ?? "",
tagIds: editingMarker?.tags.map(tag => tag.id) ?? []
};
return (
<Formik
initialValues={values}
onSubmit={onSubmit}
>
<Formik initialValues={values} onSubmit={onSubmit}>
<FormikForm>
<div>
<Form.Group className="row">
<Form.Label htmlFor="title" className="col-2">
Scene Marker Title
</Form.Label>
<Field name="title">
{renderTitleField}
</Field>
<Field name="title">{renderTitleField}</Field>
</Form.Group>
<Form.Group className="row">
<Form.Label htmlFor="primaryTagId" className="col-2">
Primary Tag
</Form.Label>
<div className="col-6">
<Field name="primaryTagId">
{renderPrimaryTagField}
</Field>
<Field name="primaryTagId">{renderPrimaryTagField}</Field>
</div>
<Form.Label htmlFor="seconds" className="col-1">Time</Form.Label>
<Field name="seconds">
{renderSecondsField}
</Field>
<Form.Label htmlFor="seconds" className="col-1">
Time
</Form.Label>
<Field name="seconds">{renderSecondsField}</Field>
</Form.Group>
<Form.Group className="row">
<Form.Label htmlFor="tagIds" className="col-2">Tags</Form.Label>
<Form.Label htmlFor="tagIds" className="col-2">
Tags
</Form.Label>
<div className="col-10">
<Field name="tagIds">
{renderTagsField}
</Field>
<Field name="tagIds">{renderTagsField}</Field>
</div>
</Form.Group>
</div>
@@ -178,4 +173,4 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMa
</FormikForm>
</Formik>
);
}
};

View File

@@ -1,12 +1,10 @@
import React, { useState } from "react";
import {
Button,
} from "react-bootstrap";
import { Button } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import { WallPanel } from "src/components/Wall/WallPanel";
import { JWUtils } from "src/utils";
import { PrimaryTags } from './PrimaryTags';
import { SceneMarkerForm } from './SceneMarkerForm';
import { PrimaryTags } from "./PrimaryTags";
import { SceneMarkerForm } from "./SceneMarkerForm";
interface ISceneMarkersPanelProps {
scene: GQL.SceneDataFragment;
@@ -17,10 +15,9 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
props: ISceneMarkersPanelProps
) => {
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
const [
editingMarker,
setEditingMarker
] = useState<GQL.SceneMarkerDataFragment>();
const [editingMarker, setEditingMarker] = useState<
GQL.SceneMarkerDataFragment
>();
const jwplayer = JWUtils.getPlayer();

View File

@@ -36,9 +36,7 @@ export const SceneMarkerList: React.FC = () => {
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
if (
singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1
) {
if (singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1) {
// navigate to the scene player page
const url = NavUtils.makeSceneMarkerUrl(
singleResult.data.findSceneMarkers.scene_markers[0]

View File

@@ -3,7 +3,7 @@ import ReactJWPlayer from "react-jw-player";
import { HotKeys } from "react-hotkeys";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { JWUtils } from 'src/utils';
import { JWUtils } from "src/utils";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
interface IScenePlayerProps {
@@ -206,7 +206,10 @@ export class ScenePlayerImpl extends React.Component<
public render() {
return (
<HotKeys keyMap={KeyMap} handlers={this.KeyHandlers} className="row">
<div id="jwplayer-container" className="w-100 col-sm-9 m-sm-auto no-gutter" >
<div
id="jwplayer-container"
className="w-100 col-sm-9 m-sm-auto no-gutter"
>
{this.renderPlayer()}
<ScenePlayerScrubber
scene={this.props.scene}

View File

@@ -3,7 +3,11 @@ import { Button, Form } from "react-bootstrap";
import _ from "lodash";
import { StashService } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { FilterSelect, StudioSelect, LoadingIndicator } from "src/components/Shared";
import {
FilterSelect,
StudioSelect,
LoadingIndicator
} from "src/components/Shared";
import { useToast } from "src/hooks";
interface IListOperationProps {
@@ -223,7 +227,6 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
setIsLoading(false);
}, [props.selected]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
@@ -249,19 +252,21 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
);
}
if(isLoading)
return <LoadingIndicator />;
if (isLoading) return <LoadingIndicator />;
function render() {
return (
<div className="operation-container">
<Form.Group controlId="rating" className="operation-item rating-operation">
<Form.Group
controlId="rating"
className="operation-item rating-operation"
>
<Form.Label>Rating</Form.Label>
<Form.Control
as="select"
onChange={(event: any) => setRating(event.target.value)}
>
{["", '1', '2', '3', '4', '5'].map(opt => (
{["", "1", "2", "3", "4", "5"].map(opt => (
<option selected={opt === rating} value={opt}>
{opt}
</option>

View File

@@ -15,7 +15,7 @@ import {
FindStudiosQueryResult,
FindPerformersQueryResult
} from "src/core/generated-graphql";
import { LoadingIndicator } from 'src/components/Shared';
import { LoadingIndicator } from "src/components/Shared";
import { ListFilter } from "src/components/list/ListFilter";
import { Pagination } from "src/components/list/Pagination";
import { StashService } from "src/core/StashService";

View File

@@ -3,4 +3,4 @@ export { default as NavUtils } from "./navigation";
export { default as TableUtils } from "./table";
export { default as TextUtils } from "./text";
export { default as DurationUtils } from "./duration";
export { default as JWUtils } from './jwplayer';
export { default as JWUtils } from "./jwplayer";

View File

@@ -1,7 +1,5 @@
const playerID = "main-jwplayer";
const getPlayer = () => (
(window as any).jwplayer(playerID)
)
const getPlayer = () => (window as any).jwplayer(playerID);
export default {
playerID,

View File

@@ -94,7 +94,11 @@ const renderHtmlSelect = (options: {
options.onChange(event.currentTarget.value)
}
>
{ options.selectOptions.map(opt => <option value={opt} key={opt}>{opt}</option>)}
{options.selectOptions.map(opt => (
<option value={opt} key={opt}>
{opt}
</option>
))}
</Form.Control>
</td>
</tr>