Merge branch 'formfield-v2' into gw/20201226-admin-formfields

This commit is contained in:
gingervitis 2021-01-31 01:00:28 -08:00
commit d01c2f081f
9 changed files with 293 additions and 137 deletions

View File

@ -2,7 +2,7 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { Typography } from 'antd'; import { Typography } from 'antd';
import ToggleSwitch from './form-toggleswitch'; import ToggleSwitch from './form-toggleswitch-with-submit';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import { FIELD_PROPS_NSFW, FIELD_PROPS_YP } from './constants'; import { FIELD_PROPS_NSFW, FIELD_PROPS_YP } from './constants';

View File

@ -1,5 +1,5 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import TextField, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './form-textfield'; import TextFieldWithSubmit, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './form-textfield-with-submit';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import { postConfigUpdateToAPI, TEXTFIELD_PROPS_USERNAME, TEXTFIELD_PROPS_INSTANCE_URL, TEXTFIELD_PROPS_SERVER_TITLE, TEXTFIELD_PROPS_STREAM_TITLE, TEXTFIELD_PROPS_SERVER_SUMMARY, TEXTFIELD_PROPS_LOGO, API_YP_SWITCH } from './constants'; import { postConfigUpdateToAPI, TEXTFIELD_PROPS_USERNAME, TEXTFIELD_PROPS_INSTANCE_URL, TEXTFIELD_PROPS_SERVER_TITLE, TEXTFIELD_PROPS_STREAM_TITLE, TEXTFIELD_PROPS_SERVER_SUMMARY, TEXTFIELD_PROPS_LOGO, API_YP_SWITCH } from './constants';
@ -47,7 +47,7 @@ export default function EditInstanceDetails() {
return ( return (
<div className={configStyles.publicDetailsContainer}> <div className={configStyles.publicDetailsContainer}>
<div className={configStyles.textFieldsSection}> <div className={configStyles.textFieldsSection}>
<TextField <TextFieldWithSubmit
fieldName="instanceUrl" fieldName="instanceUrl"
{...TEXTFIELD_PROPS_INSTANCE_URL} {...TEXTFIELD_PROPS_INSTANCE_URL}
value={formDataValues.instanceUrl} value={formDataValues.instanceUrl}
@ -57,28 +57,28 @@ export default function EditInstanceDetails() {
onSubmit={handleSubmitInstanceUrl} onSubmit={handleSubmitInstanceUrl}
/> />
<TextField <TextFieldWithSubmit
fieldName="title" fieldName="title"
{...TEXTFIELD_PROPS_SERVER_TITLE} {...TEXTFIELD_PROPS_SERVER_TITLE}
value={formDataValues.title} value={formDataValues.title}
initialValue={instanceDetails.title} initialValue={instanceDetails.title}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
<TextField <TextFieldWithSubmit
fieldName="streamTitle" fieldName="streamTitle"
{...TEXTFIELD_PROPS_STREAM_TITLE} {...TEXTFIELD_PROPS_STREAM_TITLE}
value={formDataValues.streamTitle} value={formDataValues.streamTitle}
initialValue={instanceDetails.streamTitle} initialValue={instanceDetails.streamTitle}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
<TextField <TextFieldWithSubmit
fieldName="name" fieldName="name"
{...TEXTFIELD_PROPS_USERNAME} {...TEXTFIELD_PROPS_USERNAME}
value={formDataValues.name} value={formDataValues.name}
initialValue={instanceDetails.name} initialValue={instanceDetails.name}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
<TextField <TextFieldWithSubmit
fieldName="summary" fieldName="summary"
{...TEXTFIELD_PROPS_SERVER_SUMMARY} {...TEXTFIELD_PROPS_SERVER_SUMMARY}
type={TEXTFIELD_TYPE_TEXTAREA} type={TEXTFIELD_TYPE_TEXTAREA}
@ -86,7 +86,7 @@ export default function EditInstanceDetails() {
initialValue={instanceDetails.summary} initialValue={instanceDetails.summary}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
<TextField <TextFieldWithSubmit
fieldName="logo" fieldName="logo"
{...TEXTFIELD_PROPS_LOGO} {...TEXTFIELD_PROPS_LOGO}
value={formDataValues.logo} value={formDataValues.logo}

View File

@ -1,8 +1,9 @@
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import { Button, Input, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import { CopyOutlined, RedoOutlined } from '@ant-design/icons'; import { CopyOutlined, RedoOutlined } from '@ant-design/icons';
import TextField, { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield'; import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield';
import TextFieldWithSubmit from './form-textfield-with-submit';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import { TEXTFIELD_PROPS_FFMPEG, TEXTFIELD_PROPS_RTMP_PORT, TEXTFIELD_PROPS_STREAM_KEY, TEXTFIELD_PROPS_WEB_PORT, } from './constants'; import { TEXTFIELD_PROPS_FFMPEG, TEXTFIELD_PROPS_RTMP_PORT, TEXTFIELD_PROPS_STREAM_KEY, TEXTFIELD_PROPS_WEB_PORT, } from './constants';
@ -58,7 +59,7 @@ export default function EditInstanceDetails() {
return ( return (
<div className={configStyles.publicDetailsContainer}> <div className={configStyles.publicDetailsContainer}>
<div className={configStyles.textFieldsSection}> <div className={configStyles.textFieldsSection}>
<TextField <TextFieldWithSubmit
fieldName="streamKey" fieldName="streamKey"
{...TEXTFIELD_PROPS_STREAM_KEY} {...TEXTFIELD_PROPS_STREAM_KEY}
value={formDataValues.streamKey} value={formDataValues.streamKey}
@ -87,14 +88,14 @@ export default function EditInstanceDetails() {
onClick={generateStreamKey} onClick={generateStreamKey}
/> />
</div> </div>
<TextField <TextFieldWithSubmit
fieldName="ffmpegPath" fieldName="ffmpegPath"
{...TEXTFIELD_PROPS_FFMPEG} {...TEXTFIELD_PROPS_FFMPEG}
value={formDataValues.ffmpegPath} value={formDataValues.ffmpegPath}
initialValue={ffmpegPath} initialValue={ffmpegPath}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
<TextField <TextFieldWithSubmit
fieldName="webServerPort" fieldName="webServerPort"
{...TEXTFIELD_PROPS_WEB_PORT} {...TEXTFIELD_PROPS_WEB_PORT}
value={formDataValues.webServerPort} value={formDataValues.webServerPort}
@ -102,7 +103,7 @@ export default function EditInstanceDetails() {
type={TEXTFIELD_TYPE_NUMBER} type={TEXTFIELD_TYPE_NUMBER}
onChange={handleFieldChange} onChange={handleFieldChange}
/> />
<TextField <TextFieldWithSubmit
fieldName="rtmpServerPort" fieldName="rtmpServerPort"
{...TEXTFIELD_PROPS_RTMP_PORT} {...TEXTFIELD_PROPS_RTMP_PORT}
value={formDataValues.rtmpServerPort} value={formDataValues.rtmpServerPort}

View File

@ -3,14 +3,18 @@ import React, { useContext, useState, useEffect } from 'react';
import { Typography, Tag, Input } from 'antd'; import { Typography, Tag, Input } from 'antd';
import { ServerStatusContext } from '../../../utils/server-status-context'; import { ServerStatusContext } from '../../../utils/server-status-context';
import { FIELD_PROPS_TAGS, RESET_TIMEOUT, SUCCESS_STATES, postConfigUpdateToAPI } from './constants'; import { FIELD_PROPS_TAGS, RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import TextField from './form-textfield';
import { UpdateArgs } from '../../../types/config-section';
import { createInputStatus, StatusState, STATUS_ERROR, STATUS_PROCESSING, STATUS_SUCCESS, STATUS_WARNING } from '../../../utils/input-statuses';
const { Title } = Typography; const { Title } = Typography;
export default function EditInstanceTags() { export default function EditInstanceTags() {
const [newTagInput, setNewTagInput] = useState(''); const [newTagInput, setNewTagInput] = useState('');
const [submitStatus, setSubmitStatus] = useState(null); const [fieldStatus, setFieldStatus] = useState<StatusState>(null);
const [submitStatusMessage, setSubmitStatusMessage] = useState(''); // const [submitStatus, setSubmitStatus] = useState(null);
// const [submitStatusMessage, setSubmitStatusMessage] = useState('');
const serverStatusData = useContext(ServerStatusContext); const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { serverConfig, setFieldInConfigState } = serverStatusData || {};
@ -33,37 +37,47 @@ export default function EditInstanceTags() {
}, []); }, []);
const resetStates = () => { const resetStates = () => {
setSubmitStatus(null); // setSubmitStatus(null);
setSubmitStatusMessage(''); // setSubmitStatusMessage('');
setFieldStatus(null);
resetTimer = null; resetTimer = null;
clearTimeout(resetTimer); clearTimeout(resetTimer);
} }
// posts all the tags at once as an array obj // posts all the tags at once as an array obj
const postUpdateToAPI = async (postValue: any) => { const postUpdateToAPI = async (postValue: any) => {
setFieldStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({ await postConfigUpdateToAPI({
apiPath, apiPath,
data: { value: postValue }, data: { value: postValue },
onSuccess: () => { onSuccess: () => {
setFieldInConfigState({ fieldName: 'tags', value: postValue, path: configPath }); setFieldInConfigState({ fieldName: 'tags', value: postValue, path: configPath });
setSubmitStatus('success'); setFieldStatus(createInputStatus(STATUS_SUCCESS, 'Tags updated.'));
setSubmitStatusMessage('Tags updated.');
// setSubmitStatus('success');
// setSubmitStatusMessage('Tags updated.');
setNewTagInput(''); setNewTagInput('');
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
onError: (message: string) => { onError: (message: string) => {
setSubmitStatus('error'); setFieldStatus(createInputStatus(STATUS_ERROR, message));
setSubmitStatusMessage(message);
// setSubmitStatus('error');
// setSubmitStatusMessage(message);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT); resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}, },
}); });
}; };
const handleInputChange = e => { const handleInputChange = ({ value }: UpdateArgs) => {
if (submitStatusMessage !== '') { if (!fieldStatus) {
setSubmitStatusMessage(''); setFieldStatus(null);
} }
setNewTagInput(e.target.value); // if (submitStatusMessage !== '') {
// setSubmitStatusMessage('');
// }
setNewTagInput(value);
}; };
// send to api and do stuff // send to api and do stuff
@ -71,11 +85,15 @@ export default function EditInstanceTags() {
resetStates(); resetStates();
const newTag = newTagInput.trim(); const newTag = newTagInput.trim();
if (newTag === '') { if (newTag === '') {
setSubmitStatusMessage('Please enter a tag'); setFieldStatus(createInputStatus(STATUS_WARNING, 'Please enter a tag'));
// setSubmitStatusMessage('Please enter a tag');
return; return;
} }
if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) { if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) {
setSubmitStatusMessage('This tag is already used!'); setFieldStatus(createInputStatus(STATUS_WARNING, 'This tag is already used!'));
// setSubmitStatusMessage('This tag is already used!');
return; return;
} }
@ -90,10 +108,10 @@ export default function EditInstanceTags() {
postUpdateToAPI(updatedTags); postUpdateToAPI(updatedTags);
} }
const { // const {
icon: newStatusIcon = null, // icon: newStatusIcon = null,
message: newStatusMessage = '', // message: newStatusMessage = '',
} = SUCCESS_STATES[submitStatus] || {}; // } = fieldStatus || {};
return ( return (
<div className="tag-editor-container"> <div className="tag-editor-container">
@ -111,19 +129,20 @@ export default function EditInstanceTags() {
); );
})} })}
</div> </div>
<div className={`status-message ${submitStatus || ''}`}> {/* <div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage} {newStatusIcon} {newStatusMessage} {submitStatusMessage}
</div> </div> */}
<div className="add-new-tag-section"> <div className="add-new-tag-section">
<Input <TextField
type="text" fieldName="tag-input"
className="new-tag-input"
value={newTagInput} value={newTagInput}
className="new-tag-input"
onChange={handleInputChange} onChange={handleInputChange}
onPressEnter={handleSubmitNewTag} onPressEnter={handleSubmitNewTag}
maxLength={maxLength} maxLength={maxLength}
placeholder={placeholder} placeholder={placeholder}
allowClear status={fieldStatus}
// message={`${newStatusIcon} ${newStatusMessage} ${submitStatusMessage}`}
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,137 @@
import React, { useEffect, useState, useContext } from 'react';
import { Button } from 'antd';
import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import { ServerStatusContext } from '../../../utils/server-status-context';
import TextField, { TextFieldProps } from './form-textfield';
import { createInputStatus, StatusState, STATUS_ERROR, STATUS_PROCESSING, STATUS_SUCCESS } from '../../../utils/input-statuses';
import { UpdateArgs } from '../../../types/config-section';
export const TEXTFIELD_TYPE_TEXT = 'default';
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
export const TEXTFIELD_TYPE_NUMBER = 'numeric';
export const TEXTFIELD_TYPE_TEXTAREA = 'textarea';
export const TEXTFIELD_TYPE_URL = 'url';
interface TextFieldWithSubmitProps extends TextFieldProps {
apiPath: string;
configPath?: string;
initialValue?: string;
}
export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
const [fieldStatus, setFieldStatus] = useState<StatusState>(null);
const [hasChanged, setHasChanged] = useState(false);
const [fieldValueForSubmit, setFieldValueForSubmit] = useState<string | number>('');
const serverStatusData = useContext(ServerStatusContext);
const { setFieldInConfigState } = serverStatusData || {};
let resetTimer = null;
const {
apiPath,
configPath = '',
initialValue,
...textFieldProps // rest of props
} = props;
const {
fieldName,
required,
status,
// type,
value,
onChange,
// onBlur,
onSubmit,
} = textFieldProps;
// Clear out any validation states and messaging
const resetStates = () => {
setFieldStatus(null);
setHasChanged(false);
clearTimeout(resetTimer);
resetTimer = null;
};
useEffect(() => {
// TODO: Add native validity checks here, somehow
// https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
// const hasValidity = (type !== TEXTFIELD_TYPE_NUMBER && e.target.validity.valid) || type === TEXTFIELD_TYPE_NUMBER ;
if ((required && (value === '' || value === null)) || value === initialValue) {
setHasChanged(false);
} else {
// show submit button
resetStates();
setHasChanged(true);
setFieldValueForSubmit(value);
}
}, [value]);
// if field is required but value is empty, or equals initial value, then don't show submit/update button. otherwise clear out any result messaging and display button.
const handleChange = ({ fieldName: changedFieldName, value: changedValue }: UpdateArgs) => {
if (onChange) {
onChange({ fieldName: changedFieldName, value: changedValue });
}
};
// if you blur a required field with an empty value, restore its original value in state (parent's state), if an onChange from parent is available.
const handleBlur = ({ value: changedValue }: UpdateArgs) => {
if (onChange && required && changedValue === '') {
onChange({ fieldName, value: initialValue });
}
};
// how to get current value of input
const handleSubmit = async () => {
if ((required && fieldValueForSubmit !== '') || fieldValueForSubmit !== initialValue) {
setFieldStatus(createInputStatus(STATUS_PROCESSING));
// setSubmitStatus('validating');
await postConfigUpdateToAPI({
apiPath,
data: { value: fieldValueForSubmit },
onSuccess: () => {
setFieldInConfigState({ fieldName, value: fieldValueForSubmit, path: configPath });
setFieldStatus(createInputStatus(STATUS_SUCCESS));
// setSubmitStatus('success');
},
onError: (message: string) => {
setFieldStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`));
// setSubmitStatus('error');
// setSubmitStatusMessage(`There was an error: ${message}`);
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
// if an extra onSubmit handler was sent in as a prop, let's run that too.
if (onSubmit) {
onSubmit();
}
}
}
return (
<div className="textfield-with-submit-container">
<TextField
{...textFieldProps}
status={status || fieldStatus}
onSubmit={null}
onBlur={handleBlur}
onChange={handleChange}
/>
{ hasChanged ? <Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>Update</Button> : null }
</div>
);
}
TextFieldWithSubmit.defaultProps = {
configPath: '',
initialValue: '',
};

View File

@ -1,94 +1,57 @@
import React, { useEffect, useState, useContext } from 'react'; import React from 'react';
import { Button, Input, InputNumber } from 'antd'; import { Input, InputNumber } from 'antd';
import { FormItemProps } from 'antd/es/form';
import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import { FieldUpdaterFunc } from '../../../types/config-section'; import { FieldUpdaterFunc } from '../../../types/config-section';
import { ServerStatusContext } from '../../../utils/server-status-context';
import InfoTip from '../info-tip'; import InfoTip from '../info-tip';
import { StatusState } from '../../../utils/input-statuses';
export const TEXTFIELD_TYPE_TEXT = 'default'; export const TEXTFIELD_TYPE_TEXT = 'default';
export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password export const TEXTFIELD_TYPE_PASSWORD = 'password'; // Input.Password
export const TEXTFIELD_TYPE_NUMBER = 'numeric'; export const TEXTFIELD_TYPE_NUMBER = 'numeric'; // InputNumber
export const TEXTFIELD_TYPE_TEXTAREA = 'textarea'; export const TEXTFIELD_TYPE_TEXTAREA = 'textarea'; // Input.TextArea
export const TEXTFIELD_TYPE_URL = 'url'; export const TEXTFIELD_TYPE_URL = 'url';
interface TextFieldProps { export interface TextFieldProps {
apiPath: string;
fieldName: string; fieldName: string;
configPath?: string; onSubmit?: () => void;
onPressEnter?: () => void;
className?: string;
disabled?: boolean; disabled?: boolean;
initialValue?: string;
label?: string; label?: string;
maxLength?: number; maxLength?: number;
placeholder?: string; placeholder?: string;
required?: boolean; required?: boolean;
status?: StatusState;
tip?: string; tip?: string;
type?: string; type?: string;
value?: string | number; value?: string | number;
onSubmit?: () => void; onBlur?: FieldUpdaterFunc;
onBlur?: () => void;
onChange?: FieldUpdaterFunc; onChange?: FieldUpdaterFunc;
} }
export default function TextField(props: TextFieldProps) { export default function TextField(props: TextFieldProps) {
const [submitStatus, setSubmitStatus] = useState<FormItemProps['validateStatus']>('');
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
const [hasChanged, setHasChanged] = useState(false);
const [fieldValueForSubmit, setFieldValueForSubmit] = useState<string | number>('');
const serverStatusData = useContext(ServerStatusContext);
const { setFieldInConfigState } = serverStatusData || {};
let resetTimer = null;
const { const {
apiPath, className,
configPath = '', disabled,
disabled = false,
fieldName, fieldName,
initialValue,
label, label,
maxLength, maxLength,
onBlur, onBlur,
onChange, onChange,
onSubmit, onPressEnter,
placeholder, placeholder,
required, required,
status,
tip, tip,
type, type,
value, value,
} = props; } = props;
// Clear out any validation states and messaging
const resetStates = () => {
setSubmitStatus('');
setHasChanged(false);
clearTimeout(resetTimer);
resetTimer = null;
};
useEffect(() => {
// TODO: Add native validity checks here, somehow
// https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
// const hasValidity = (type !== TEXTFIELD_TYPE_NUMBER && e.target.validity.valid) || type === TEXTFIELD_TYPE_NUMBER ;
if ((required && (value === '' || value === null)) || value === initialValue) {
setHasChanged(false);
} else {
// show submit button
resetStates();
setHasChanged(true);
setFieldValueForSubmit(value);
}
}, [value]);
// if field is required but value is empty, or equals initial value, then don't show submit/update button. otherwise clear out any result messaging and display button. // if field is required but value is empty, or equals initial value, then don't show submit/update button. otherwise clear out any result messaging and display button.
const handleChange = (e: any) => { const handleChange = (e: any) => {
const val = type === TEXTFIELD_TYPE_NUMBER ? e : e.target.value; const val = type === TEXTFIELD_TYPE_NUMBER ? e : e.target.value;
// if an extra onChange handler was sent in as a prop, let's run that too. // if an extra onChange handler was sent in as a prop, let's run that too.
if (onChange) { if (onChange) {
onChange({ fieldName, value: val }); onChange({ fieldName, value: val });
@ -96,45 +59,19 @@ export default function TextField(props: TextFieldProps) {
}; };
// if you blur a required field with an empty value, restore its original value in state (parent's state), if an onChange from parent is available. // if you blur a required field with an empty value, restore its original value in state (parent's state), if an onChange from parent is available.
const handleBlur = e => { const handleBlur = (e: any) => {
if (!onChange) {
return;
}
const val = e.target.value; const val = e.target.value;
if (required && val === '') {
onChange({ fieldName, value: initialValue });
}
// if an extra onBlur handler was sent in as a prop, let's run that too.
if (onBlur) { if (onBlur) {
onBlur(); onBlur({ value: val });
} }
}; };
// how to get current value of input const handlePressEnter = () => {
const handleSubmit = async () => { if (onPressEnter) {
if ((required && fieldValueForSubmit !== '') || fieldValueForSubmit !== initialValue) { onPressEnter();
setSubmitStatus('validating'); }
}
await postConfigUpdateToAPI({
apiPath,
data: { value: fieldValueForSubmit },
onSuccess: () => {
setFieldInConfigState({ fieldName, value: fieldValueForSubmit, path: configPath });
setSubmitStatus('success');
},
onError: (message: string) => {
setSubmitStatus('error');
setSubmitStatusMessage(`There was an error: ${message}`);
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
// if an extra onSubmit handler was sent in as a prop, let's run that too.
if (onSubmit) {
onSubmit();
}
}
}
// display the appropriate Ant text field // display the appropriate Ant text field
let Field = Input as typeof Input | typeof InputNumber | typeof Input.TextArea | typeof Input.Password; let Field = Input as typeof Input | typeof InputNumber | typeof Input.TextArea | typeof Input.Password;
@ -157,7 +94,7 @@ export default function TextField(props: TextFieldProps) {
max: (10**maxLength) - 1, max: (10**maxLength) - 1,
onKeyDown: (e: React.KeyboardEvent) => { onKeyDown: (e: React.KeyboardEvent) => {
if (e.target.value.length > maxLength - 1 ) if (e.target.value.length > maxLength - 1 )
e.preventDefault() e.preventDefault();
return false; return false;
} }
}; };
@ -169,6 +106,8 @@ export default function TextField(props: TextFieldProps) {
const fieldId = `field-${fieldName}`; const fieldId = `field-${fieldName}`;
const { icon: statusIcon, message: statusMessage } = status || {};
return ( return (
<div className={`textfield-container type-${type}`}> <div className={`textfield-container type-${type}`}>
{ required ? <span className="required-label">*</span> : null } { required ? <span className="required-label">*</span> : null }
@ -176,39 +115,41 @@ export default function TextField(props: TextFieldProps) {
<div className="textfield"> <div className="textfield">
<Field <Field
id={fieldId} id={fieldId}
className={`field ${fieldId}`} className={`field ${className} ${fieldId}`}
{...fieldProps} {...fieldProps}
allowClear allowClear
placeholder={placeholder} placeholder={placeholder}
maxLength={maxLength} maxLength={maxLength}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
onPressEnter={handlePressEnter}
disabled={disabled} disabled={disabled}
value={value} value={value}
/> />
</div> </div>
<InfoTip tip={tip} /> <InfoTip tip={tip} />
{submitStatus} { status ? statusMessage : null }
{submitStatusMessage} { status ? statusIcon : null }
{ hasChanged ? <Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>Update</Button> : null }
</div> </div>
); );
} }
TextField.defaultProps = { TextField.defaultProps = {
configPath: '', className: '',
// configPath: '',
disabled: false, disabled: false,
initialValue: '', // initialValue: '',
label: '', label: '',
maxLength: null, maxLength: null,
placeholder: '', placeholder: '',
required: false, required: false,
status: null,
tip: '', tip: '',
type: TEXTFIELD_TYPE_TEXT, type: TEXTFIELD_TYPE_TEXT,
value: '', value: '',
onSubmit: () => {}, onSubmit: () => {},
onBlur: () => {}, onBlur: () => {},
onChange: () => {}, onChange: () => {},
onPressEnter: () => {},
}; };

View File

@ -15,8 +15,8 @@ import { ServerStatusContext } from "../utils/server-status-context";
import StatisticItem from "./components/statistic" import StatisticItem from "./components/statistic"
import LogTable from "./components/log-table"; import LogTable from "./components/log-table";
import Offline from './offline-notice'; import Offline from './offline-notice';
import TextField from './components/config/form-textfield'; import TextFieldWithSubmit from './components/config/form-textfield-with-submit';
import { API_STREAM_TITLE, postConfigUpdateToAPI, TEXTFIELD_PROPS_STREAM_TITLE } from './components/config/constants'; import { TEXTFIELD_PROPS_STREAM_TITLE } from './components/config/constants';
import { import {
LOGS_WARN, LOGS_WARN,
@ -158,7 +158,7 @@ export default function Home() {
<div className="section online-details-section"> <div className="section online-details-section">
<Card title="Stream description"> <Card title="Stream description">
<TextField <TextFieldWithSubmit
fieldName="streamTitle" fieldName="streamTitle"
{...TEXTFIELD_PROPS_STREAM_TITLE} {...TEXTFIELD_PROPS_STREAM_TITLE}
value={streamTitle} value={streamTitle}

View File

@ -0,0 +1,58 @@
import { CheckCircleFilled, ExclamationCircleFilled, LoadingOutlined, WarningOutlined } from '@ant-design/icons';
export const STATUS_RESET_TIMEOUT = 3000;
export const STATUS_ERROR = 'error';
export const STATUS_INVALID = 'invalid';
export const STATUS_PROCESSING = 'proessing';
export const STATUS_SUCCESS = 'success';
export const STATUS_WARNING = 'warning';
export type InputStatusTypes =
typeof STATUS_ERROR |
typeof STATUS_INVALID |
typeof STATUS_PROCESSING |
typeof STATUS_SUCCESS |
typeof STATUS_WARNING;
export type StatusState = {
icon: any; // Element type of sorts?
message: string;
};
export const INPUT_STATES = {
[STATUS_SUCCESS]: {
icon: <CheckCircleFilled style={{ color: 'green' }} />,
message: 'Success!',
},
[STATUS_ERROR]: {
icon: <ExclamationCircleFilled style={{ color: 'red' }} />,
message: 'An error occurred.',
},
[STATUS_INVALID]: {
icon: <ExclamationCircleFilled style={{ color: 'red' }} />,
message: 'An error occurred.',
},
[STATUS_PROCESSING]: {
icon: <LoadingOutlined />,
message: '',
},
[STATUS_WARNING]: {
icon: <WarningOutlined style={{ color: '#fc0' }} />,
message: '',
},
};
// Don't like any of the default messages in INPUT_STATES? Create a state with custom message by providing an icon style with your message.
export function createInputStatus(type: InputStatusTypes, message?: string): StatusState {
if (!type || !INPUT_STATES[type]) {
return null;
}
if (message === null) {
return INPUT_STATES[type];
}
return {
icon: INPUT_STATES[type].icon,
message,
};
}