mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Performer and Movie UI fixes and improvements (#447)
* Improve gender display * Sanitise performer URLs * Refactor editable text into separate module * Make movie duration DurationInput * Fix clearing sometimes not firing onChange * Set movie duration as string * Fix TextUtil.fileSize * Improve scene URL
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { Table, Form } from "react-bootstrap";
|
||||
import { TableUtils, ImageUtils } from "src/utils";
|
||||
import { TableUtils, ImageUtils, EditableTextUtils, TextUtils } from "src/utils";
|
||||
import { MovieScenesPanel } from "./MovieScenesPanel";
|
||||
|
||||
export const Movie: React.FC = () => {
|
||||
@@ -206,11 +206,14 @@ export const Movie: React.FC = () => {
|
||||
isEditing,
|
||||
onChange: setAliases,
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
{TableUtils.renderDurationInput({
|
||||
title: "Duration",
|
||||
value: duration,
|
||||
isEditing,
|
||||
onChange: setDuration,
|
||||
onChange: (value: string | undefined) => {
|
||||
setDuration(value ?? "");
|
||||
},
|
||||
asString: true,
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Date (YYYY-MM-DD)",
|
||||
@@ -236,14 +239,14 @@ export const Movie: React.FC = () => {
|
||||
|
||||
<Form.Group controlId="url">
|
||||
<Form.Label>URL</Form.Label>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
readOnly={!isEditing}
|
||||
onChange={(newValue: React.FormEvent<HTMLTextAreaElement>) =>
|
||||
setUrl(newValue.currentTarget.value)
|
||||
}
|
||||
value={url}
|
||||
/>
|
||||
<div>
|
||||
{EditableTextUtils.renderInputGroup({
|
||||
isEditing,
|
||||
onChange: setUrl,
|
||||
value: url,
|
||||
url: TextUtils.sanitiseURL(url),
|
||||
})}
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="synopsis">
|
||||
|
||||
@@ -177,7 +177,7 @@ export const Performer: React.FC = () => {
|
||||
{performer.url && (
|
||||
<Button className="minimal">
|
||||
<a
|
||||
href={performer.url}
|
||||
href={TextUtils.sanitiseURL(performer.url)}
|
||||
className="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -189,7 +189,7 @@ export const Performer: React.FC = () => {
|
||||
{performer.twitter && (
|
||||
<Button className="minimal">
|
||||
<a
|
||||
href={`https://www.twitter.com/${performer.twitter}`}
|
||||
href={TextUtils.sanitiseURL(performer.twitter, TextUtils.twitterURL)}
|
||||
className="twitter"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -201,7 +201,7 @@ export const Performer: React.FC = () => {
|
||||
{performer.instagram && (
|
||||
<Button className="minimal">
|
||||
<a
|
||||
href={`https://www.instagram.com/${performer.instagram}`}
|
||||
href={TextUtils.sanitiseURL(performer.instagram, TextUtils.instagramURL)}
|
||||
className="instagram"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -213,11 +213,19 @@ export const Performer: React.FC = () => {
|
||||
</span>
|
||||
);
|
||||
|
||||
function renderPerformerImage() {
|
||||
if (imagePreview) {
|
||||
return (
|
||||
<img className="photo" src={imagePreview} alt="Performer" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderNewView() {
|
||||
return (
|
||||
<div className="row new-view">
|
||||
<div className="col-4">
|
||||
<img className="photo" src={imagePreview} alt="Performer" />
|
||||
{renderPerformerImage()}
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<h2>Create Performer</h2>
|
||||
@@ -242,11 +250,6 @@ export const Performer: React.FC = () => {
|
||||
</div>
|
||||
<div className="col col-sm-6">
|
||||
<div className="row">
|
||||
<div className="image-container col-6 d-block d-sm-none">
|
||||
<Button variant="link" onClick={() => setLightboxIsOpen(true)}>
|
||||
<img className="performer" src={imagePreview} alt="Performer" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="performer-head col-6 col-sm-12">
|
||||
<h2>
|
||||
{performer.name}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form, Popover, OverlayTrigger, Table } from "react-bootstrap";
|
||||
import { Button, Popover, OverlayTrigger, Table } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import {
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ScrapePerformerSuggest,
|
||||
LoadingIndicator,
|
||||
} from "src/components/Shared";
|
||||
import { ImageUtils, TableUtils } from "src/utils";
|
||||
import { ImageUtils, TableUtils, TextUtils, EditableTextUtils } from "src/utils";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
interface IPerformerDetails {
|
||||
@@ -323,16 +323,13 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
{maybeRenderScrapeButton()}
|
||||
</td>
|
||||
<td>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
value={url ?? ""}
|
||||
readOnly={!isEditing}
|
||||
plaintext={!isEditing}
|
||||
placeholder="URL"
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
setUrl(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
{EditableTextUtils.renderInputGroup({
|
||||
title: "URL",
|
||||
value: url,
|
||||
url: TextUtils.sanitiseURL(url),
|
||||
isEditing: !!isEditing,
|
||||
onChange: setUrl,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -486,12 +483,14 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Twitter",
|
||||
value: twitter,
|
||||
url: TextUtils.sanitiseURL(twitter, TextUtils.twitterURL),
|
||||
isEditing: !!isEditing,
|
||||
onChange: setTwitter,
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Instagram",
|
||||
value: instagram,
|
||||
url: TextUtils.sanitiseURL(instagram, TextUtils.instagramURL),
|
||||
isEditing: !!isEditing,
|
||||
onChange: setInstagram,
|
||||
})}
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#url-field {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#performer-page {
|
||||
flex-direction: row;
|
||||
margin: 10px auto;
|
||||
|
||||
@@ -150,7 +150,11 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Downloaded From</span>
|
||||
<span className="col-8 text-truncate">{props.scene.url}</span>
|
||||
<span className="col-8 text-truncate">
|
||||
<a href={TextUtils.sanitiseURL(props.scene.url)}>
|
||||
{props.scene.url}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
)
|
||||
}
|
||||
numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)}
|
||||
mandatory={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
<DurationInput
|
||||
className="row col col-4"
|
||||
numericValue={maximumLoopDuration}
|
||||
onValueChange={(duration) => setMaximumLoopDuration(duration)}
|
||||
onValueChange={(duration) => setMaximumLoopDuration(duration ?? 0)}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Maximum scene duration where scene player will loop the video - 0 to
|
||||
|
||||
@@ -5,54 +5,69 @@ import { DurationUtils } from "src/utils";
|
||||
|
||||
interface IProps {
|
||||
disabled?: boolean;
|
||||
numericValue: number;
|
||||
onValueChange(valueAsNumber: number): void;
|
||||
numericValue: number | undefined;
|
||||
mandatory?: boolean;
|
||||
onValueChange(valueAsNumber: number | undefined, valueAsString?: string): void;
|
||||
onReset?(): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
const [value, setValue] = useState<string>(
|
||||
DurationUtils.secondsToString(props.numericValue)
|
||||
const [value, setValue] = useState<string | undefined>(
|
||||
props.numericValue !== undefined ? DurationUtils.secondsToString(props.numericValue) : undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(DurationUtils.secondsToString(props.numericValue));
|
||||
}, [props.numericValue]);
|
||||
if (props.numericValue !== undefined || props.mandatory) {
|
||||
setValue(DurationUtils.secondsToString(props.numericValue ?? 0));
|
||||
} else {
|
||||
setValue(undefined);
|
||||
}
|
||||
}, [props.numericValue, props.mandatory]);
|
||||
|
||||
function increment() {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let seconds = DurationUtils.stringToSeconds(value);
|
||||
seconds += 1;
|
||||
props.onValueChange(seconds);
|
||||
props.onValueChange(seconds, DurationUtils.secondsToString(seconds));
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let seconds = DurationUtils.stringToSeconds(value);
|
||||
seconds -= 1;
|
||||
props.onValueChange(seconds);
|
||||
props.onValueChange(seconds, DurationUtils.secondsToString(seconds));
|
||||
}
|
||||
|
||||
function renderButtons() {
|
||||
return (
|
||||
<ButtonGroup vertical>
|
||||
<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()}
|
||||
>
|
||||
<Icon icon="chevron-down" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
if (!props.disabled) {
|
||||
return (
|
||||
<ButtonGroup vertical>
|
||||
<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()}
|
||||
>
|
||||
<Icon icon="chevron-down" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
@@ -81,10 +96,14 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
setValue(e.currentTarget.value)
|
||||
}
|
||||
onBlur={() =>
|
||||
props.onValueChange(DurationUtils.stringToSeconds(value))
|
||||
}
|
||||
placeholder="hh:mm:ss"
|
||||
onBlur={() => {
|
||||
if (props.mandatory || (value !== undefined && value !== "")) {
|
||||
props.onValueChange(DurationUtils.stringToSeconds(value), value);
|
||||
} else {
|
||||
props.onValueChange(undefined);
|
||||
}
|
||||
}}
|
||||
placeholder={!props.disabled ? "hh:mm:ss" : undefined}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
{maybeRenderReset()}
|
||||
|
||||
@@ -14,7 +14,7 @@ const secondsToString = (seconds: number) => {
|
||||
return ret;
|
||||
};
|
||||
|
||||
const stringToSeconds = (v: string) => {
|
||||
const stringToSeconds = (v?: string) => {
|
||||
if (!v) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
206
ui/v2.5/src/utils/editabletext.tsx
Normal file
206
ui/v2.5/src/utils/editabletext.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { FilterSelect, DurationInput } from "src/components/Shared";
|
||||
import { DurationUtils } from ".";
|
||||
|
||||
const renderTextArea = (options: {
|
||||
value: string | undefined;
|
||||
isEditing: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
as="textarea"
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLTextAreaElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
value={options.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderEditableText = (options: {
|
||||
title?: string;
|
||||
value?: string | number;
|
||||
isEditing: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Form.Control
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
value={
|
||||
typeof options.value === "number"
|
||||
? options.value.toString()
|
||||
: options.value
|
||||
}
|
||||
placeholder={options.title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const renderInputGroup = (options: {
|
||||
title?: string;
|
||||
placeholder?: string;
|
||||
value: string | undefined;
|
||||
isEditing: boolean;
|
||||
url?: string;
|
||||
onChange: (value: string) => void;
|
||||
}) => {
|
||||
if (options.url && !options.isEditing) {
|
||||
return (
|
||||
<a
|
||||
href={options.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{options.value}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
value={options.value}
|
||||
placeholder={options.placeholder ?? options.title}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderDurationInput = (options: {
|
||||
value: string | undefined;
|
||||
isEditing: boolean;
|
||||
url?: string;
|
||||
asString?: boolean
|
||||
onChange: (value: string | undefined) => void;
|
||||
}) => {
|
||||
let numericValue: number | undefined = undefined;
|
||||
if (options.value) {
|
||||
if (!options.asString) {
|
||||
try {
|
||||
numericValue = Number.parseInt(options.value, 10);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
numericValue = DurationUtils.stringToSeconds(options.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.isEditing) {
|
||||
let durationString = undefined;
|
||||
if (numericValue !== undefined) {
|
||||
durationString = DurationUtils.secondsToString(numericValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
readOnly={true}
|
||||
plaintext={true}
|
||||
defaultValue={durationString}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DurationInput
|
||||
disabled={!options.isEditing}
|
||||
numericValue={numericValue}
|
||||
onValueChange={(valueAsNumber: number, valueAsString? : string) => {
|
||||
if (!options.asString) {
|
||||
valueAsString = valueAsNumber !== undefined ? valueAsNumber.toString() : undefined;
|
||||
}
|
||||
|
||||
options.onChange(valueAsString);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderHtmlSelect = (options: {
|
||||
value?: string | number;
|
||||
isEditing: boolean;
|
||||
onChange: (value: string) => void;
|
||||
selectOptions: Array<string | number>;
|
||||
}) => {
|
||||
if (!options.isEditing) {
|
||||
return (
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
readOnly={true}
|
||||
plaintext={true}
|
||||
defaultValue={options.value}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
disabled={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
value={options.value?.toString()}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
>
|
||||
{options.selectOptions.map((opt) => (
|
||||
<option value={opt} key={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: isediting
|
||||
const renderFilterSelect = (options: {
|
||||
type: "performers" | "studios" | "tags";
|
||||
initialId: string | undefined;
|
||||
onChange: (id: string | undefined) => void;
|
||||
}) => (
|
||||
<FilterSelect
|
||||
type={options.type}
|
||||
onSelect={(items) => options.onChange(items[0]?.id)}
|
||||
initialIds={options.initialId ? [options.initialId] : []}
|
||||
/>
|
||||
);
|
||||
|
||||
// TODO: isediting
|
||||
const renderMultiSelect = (options: {
|
||||
type: "performers" | "studios" | "tags";
|
||||
initialIds: string[] | undefined;
|
||||
onChange: (ids: string[]) => void;
|
||||
}) => (
|
||||
<FilterSelect
|
||||
type={options.type}
|
||||
isMulti
|
||||
onSelect={(items) => options.onChange(items.map((i) => i.id))}
|
||||
initialIds={options.initialIds ?? []}
|
||||
/>
|
||||
);
|
||||
|
||||
const EditableTextUtils = {
|
||||
renderTextArea,
|
||||
renderEditableText,
|
||||
renderInputGroup,
|
||||
renderDurationInput,
|
||||
renderHtmlSelect,
|
||||
renderFilterSelect,
|
||||
renderMultiSelect,
|
||||
};
|
||||
export default EditableTextUtils;
|
||||
@@ -2,6 +2,7 @@ export { default as ImageUtils } from "./image";
|
||||
export { default as NavUtils } from "./navigation";
|
||||
export { default as TableUtils } from "./table";
|
||||
export { default as TextUtils } from "./text";
|
||||
export { default as EditableTextUtils } from "./editabletext";
|
||||
export { default as DurationUtils } from "./duration";
|
||||
export { default as JWUtils } from "./jwplayer";
|
||||
export { default as SessionUtils } from "./session";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { FilterSelect } from "src/components/Shared";
|
||||
import EditableTextUtils from "./editabletext";
|
||||
|
||||
const renderEditableTextTableRow = (options: {
|
||||
title: string;
|
||||
@@ -11,19 +10,7 @@ const renderEditableTextTableRow = (options: {
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<Form.Control
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
value={
|
||||
typeof options.value === "number"
|
||||
? options.value.toString()
|
||||
: options.value
|
||||
}
|
||||
placeholder={options.title}
|
||||
/>
|
||||
{EditableTextUtils.renderEditableText(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -37,16 +24,7 @@ const renderTextArea = (options: {
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
as="textarea"
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLTextAreaElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
value={options.value}
|
||||
/>
|
||||
{EditableTextUtils.renderTextArea(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -56,27 +34,35 @@ const renderInputGroup = (options: {
|
||||
placeholder?: string;
|
||||
value: string | undefined;
|
||||
isEditing: boolean;
|
||||
asURL?: boolean;
|
||||
urlPrefix?: string;
|
||||
url?: string;
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
defaultValue={options.value}
|
||||
placeholder={options.placeholder ?? options.title}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
{EditableTextUtils.renderInputGroup(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
const renderDurationInput = (options: {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
value: string | undefined;
|
||||
isEditing: boolean;
|
||||
asString?: boolean;
|
||||
onChange: (value: string | undefined) => void;
|
||||
}) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
{EditableTextUtils.renderDurationInput(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHtmlSelect = (options: {
|
||||
title: string;
|
||||
value?: string | number;
|
||||
@@ -87,22 +73,7 @@ const renderHtmlSelect = (options: {
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
disabled={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
value={options.value?.toString()}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
>
|
||||
{options.selectOptions.map((opt) => (
|
||||
<option value={opt} key={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
{EditableTextUtils.renderHtmlSelect(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -117,11 +88,7 @@ const renderFilterSelect = (options: {
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<FilterSelect
|
||||
type={options.type}
|
||||
onSelect={(items) => options.onChange(items[0]?.id)}
|
||||
initialIds={options.initialId ? [options.initialId] : []}
|
||||
/>
|
||||
{EditableTextUtils.renderFilterSelect(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -136,12 +103,7 @@ const renderMultiSelect = (options: {
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<FilterSelect
|
||||
type={options.type}
|
||||
isMulti
|
||||
onSelect={(items) => options.onChange(items.map((i) => i.id))}
|
||||
initialIds={options.initialIds ?? []}
|
||||
/>
|
||||
{EditableTextUtils.renderMultiSelect(options)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -150,6 +112,7 @@ const Table = {
|
||||
renderEditableTextTableRow,
|
||||
renderTextArea,
|
||||
renderInputGroup,
|
||||
renderDurationInput,
|
||||
renderHtmlSelect,
|
||||
renderFilterSelect,
|
||||
renderMultiSelect,
|
||||
|
||||
@@ -20,7 +20,7 @@ const fileSize = (bytes: number = 0, precision: number = 2) => {
|
||||
unit++;
|
||||
}
|
||||
|
||||
return `${bytes.toFixed(+precision)} ${Units[unit]}`;
|
||||
return `${count.toFixed(+precision)} ${Units[unit]}`;
|
||||
};
|
||||
|
||||
const secondsToTimestamp = (seconds: number) => {
|
||||
@@ -83,6 +83,33 @@ const resolution = (height: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
const twitterURL = new URL("https://www.twitter.com");
|
||||
const instagramURL = new URL("https://www.instagram.com");
|
||||
|
||||
const sanitiseURL = (url?: string, siteURL?: URL) => {
|
||||
if (!url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
// just return the entire URL
|
||||
return url;
|
||||
}
|
||||
|
||||
if (siteURL) {
|
||||
// if url starts with the site host, then prepend the protocol
|
||||
if (url.startsWith(siteURL.host)) {
|
||||
return siteURL.protocol + url;
|
||||
}
|
||||
|
||||
// otherwise, construct the url from the protocol, host and passed url
|
||||
return siteURL.protocol + siteURL.host + "/" + url;
|
||||
}
|
||||
|
||||
// just prepend the protocol - assume https
|
||||
return "https://" + url;
|
||||
}
|
||||
|
||||
const TextUtils = {
|
||||
truncate,
|
||||
fileSize,
|
||||
@@ -91,6 +118,9 @@ const TextUtils = {
|
||||
age: getAge,
|
||||
bitRate,
|
||||
resolution,
|
||||
sanitiseURL,
|
||||
twitterURL,
|
||||
instagramURL,
|
||||
};
|
||||
|
||||
export default TextUtils;
|
||||
|
||||
Reference in New Issue
Block a user