diff --git a/web/pages/components/config/edit-directory.tsx b/web/pages/components/config/edit-directory.tsx index fcd4664a7..08608cf10 100644 --- a/web/pages/components/config/edit-directory.tsx +++ b/web/pages/components/config/edit-directory.tsx @@ -2,7 +2,7 @@ import React, { useState, useContext, useEffect } from 'react'; import { Typography } from 'antd'; -import ToggleSwitch from './form-toggleswitch'; +import ToggleSwitch from './form-toggleswitch-with-submit'; import { ServerStatusContext } from '../../../utils/server-status-context'; import { FIELD_PROPS_NSFW, FIELD_PROPS_YP } from './constants'; diff --git a/web/pages/components/config/edit-instance-details.tsx b/web/pages/components/config/edit-instance-details.tsx index 3ec5129d2..af2821836 100644 --- a/web/pages/components/config/edit-instance-details.tsx +++ b/web/pages/components/config/edit-instance-details.tsx @@ -1,5 +1,5 @@ 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 { 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 (
- - - - - -
-
- - - (null); + // const [submitStatus, setSubmitStatus] = useState(null); + // const [submitStatusMessage, setSubmitStatusMessage] = useState(''); const serverStatusData = useContext(ServerStatusContext); const { serverConfig, setFieldInConfigState } = serverStatusData || {}; @@ -33,37 +37,47 @@ export default function EditInstanceTags() { }, []); const resetStates = () => { - setSubmitStatus(null); - setSubmitStatusMessage(''); + // setSubmitStatus(null); + // setSubmitStatusMessage(''); + setFieldStatus(null); resetTimer = null; clearTimeout(resetTimer); } // posts all the tags at once as an array obj const postUpdateToAPI = async (postValue: any) => { + setFieldStatus(createInputStatus(STATUS_PROCESSING)); + await postConfigUpdateToAPI({ apiPath, data: { value: postValue }, onSuccess: () => { setFieldInConfigState({ fieldName: 'tags', value: postValue, path: configPath }); - setSubmitStatus('success'); - setSubmitStatusMessage('Tags updated.'); + setFieldStatus(createInputStatus(STATUS_SUCCESS, 'Tags updated.')); + + // setSubmitStatus('success'); + // setSubmitStatusMessage('Tags updated.'); setNewTagInput(''); resetTimer = setTimeout(resetStates, RESET_TIMEOUT); }, onError: (message: string) => { - setSubmitStatus('error'); - setSubmitStatusMessage(message); + setFieldStatus(createInputStatus(STATUS_ERROR, message)); + + // setSubmitStatus('error'); + // setSubmitStatusMessage(message); resetTimer = setTimeout(resetStates, RESET_TIMEOUT); }, }); }; - const handleInputChange = e => { - if (submitStatusMessage !== '') { - setSubmitStatusMessage(''); + const handleInputChange = ({ value }: UpdateArgs) => { + if (!fieldStatus) { + setFieldStatus(null); } - setNewTagInput(e.target.value); + // if (submitStatusMessage !== '') { + // setSubmitStatusMessage(''); + // } + setNewTagInput(value); }; // send to api and do stuff @@ -71,11 +85,15 @@ export default function EditInstanceTags() { resetStates(); const newTag = newTagInput.trim(); if (newTag === '') { - setSubmitStatusMessage('Please enter a tag'); + setFieldStatus(createInputStatus(STATUS_WARNING, 'Please enter a tag')); + + // setSubmitStatusMessage('Please enter a tag'); return; } 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; } @@ -90,10 +108,10 @@ export default function EditInstanceTags() { postUpdateToAPI(updatedTags); } - const { - icon: newStatusIcon = null, - message: newStatusMessage = '', - } = SUCCESS_STATES[submitStatus] || {}; + // const { + // icon: newStatusIcon = null, + // message: newStatusMessage = '', + // } = fieldStatus || {}; return (
@@ -111,19 +129,20 @@ export default function EditInstanceTags() { ); })}
-
+ {/*
{newStatusIcon} {newStatusMessage} {submitStatusMessage} -
+
*/}
-
diff --git a/web/pages/components/config/form-textfield-with-submit.tsx b/web/pages/components/config/form-textfield-with-submit.tsx new file mode 100644 index 000000000..5044225b3 --- /dev/null +++ b/web/pages/components/config/form-textfield-with-submit.tsx @@ -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(null); + + const [hasChanged, setHasChanged] = useState(false); + const [fieldValueForSubmit, setFieldValueForSubmit] = useState(''); + + 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 ( +
+ + + { hasChanged ? : null } +
+ ); +} + +TextFieldWithSubmit.defaultProps = { + configPath: '', + initialValue: '', +}; diff --git a/web/pages/components/config/form-textfield.tsx b/web/pages/components/config/form-textfield.tsx index 95c82cae0..cdb6e6ca1 100644 --- a/web/pages/components/config/form-textfield.tsx +++ b/web/pages/components/config/form-textfield.tsx @@ -1,94 +1,57 @@ -import React, { useEffect, useState, useContext } from 'react'; -import { Button, Input, InputNumber } from 'antd'; -import { FormItemProps } from 'antd/es/form'; - -import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants'; - +import React from 'react'; +import { Input, InputNumber } from 'antd'; import { FieldUpdaterFunc } from '../../../types/config-section'; -import { ServerStatusContext } from '../../../utils/server-status-context'; import InfoTip from '../info-tip'; +import { StatusState } from '../../../utils/input-statuses'; 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_NUMBER = 'numeric'; // InputNumber +export const TEXTFIELD_TYPE_TEXTAREA = 'textarea'; // Input.TextArea export const TEXTFIELD_TYPE_URL = 'url'; -interface TextFieldProps { - apiPath: string; +export interface TextFieldProps { fieldName: string; + + onSubmit?: () => void; + onPressEnter?: () => void; - configPath?: string; + className?: string; disabled?: boolean; - initialValue?: string; label?: string; maxLength?: number; placeholder?: string; required?: boolean; + status?: StatusState; tip?: string; type?: string; value?: string | number; - onSubmit?: () => void; - onBlur?: () => void; + onBlur?: FieldUpdaterFunc; onChange?: FieldUpdaterFunc; } export default function TextField(props: TextFieldProps) { - const [submitStatus, setSubmitStatus] = useState(''); - const [submitStatusMessage, setSubmitStatusMessage] = useState(''); - const [hasChanged, setHasChanged] = useState(false); - const [fieldValueForSubmit, setFieldValueForSubmit] = useState(''); - - const serverStatusData = useContext(ServerStatusContext); - const { setFieldInConfigState } = serverStatusData || {}; - - let resetTimer = null; - const { - apiPath, - configPath = '', - disabled = false, + className, + disabled, fieldName, - initialValue, label, maxLength, onBlur, onChange, - onSubmit, + onPressEnter, placeholder, required, + status, tip, type, value, } = 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. const handleChange = (e: any) => { 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 (onChange) { 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. - const handleBlur = e => { - if (!onChange) { - return; - } + const handleBlur = (e: any) => { 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) { - onBlur(); + onBlur({ value: val }); } }; - // how to get current value of input - const handleSubmit = async () => { - if ((required && fieldValueForSubmit !== '') || fieldValueForSubmit !== initialValue) { - 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(); - } + const handlePressEnter = () => { + if (onPressEnter) { + onPressEnter(); } } + // display the appropriate Ant text field 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, onKeyDown: (e: React.KeyboardEvent) => { if (e.target.value.length > maxLength - 1 ) - e.preventDefault() + e.preventDefault(); return false; } }; @@ -169,46 +106,50 @@ export default function TextField(props: TextFieldProps) { const fieldId = `field-${fieldName}`; - return ( + const { icon: statusIcon, message: statusMessage } = status || {}; + + return (
{ required ? * : null }
- {submitStatus} - {submitStatusMessage} - - { hasChanged ? : null } - + { status ? statusMessage : null } + { status ? statusIcon : null }
); } TextField.defaultProps = { - configPath: '', + className: '', + // configPath: '', disabled: false, - initialValue: '', + // initialValue: '', label: '', maxLength: null, + placeholder: '', required: false, + status: null, tip: '', type: TEXTFIELD_TYPE_TEXT, value: '', onSubmit: () => {}, onBlur: () => {}, onChange: () => {}, + onPressEnter: () => {}, }; diff --git a/web/pages/components/config/form-toggleswitch.tsx b/web/pages/components/config/form-toggleswitch-with-submit.tsx similarity index 100% rename from web/pages/components/config/form-toggleswitch.tsx rename to web/pages/components/config/form-toggleswitch-with-submit.tsx diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 13adcd505..9df1805e2 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -15,8 +15,8 @@ import { ServerStatusContext } from "../utils/server-status-context"; import StatisticItem from "./components/statistic" import LogTable from "./components/log-table"; import Offline from './offline-notice'; -import TextField from './components/config/form-textfield'; -import { API_STREAM_TITLE, postConfigUpdateToAPI, TEXTFIELD_PROPS_STREAM_TITLE } from './components/config/constants'; +import TextFieldWithSubmit from './components/config/form-textfield-with-submit'; +import { TEXTFIELD_PROPS_STREAM_TITLE } from './components/config/constants'; import { LOGS_WARN, @@ -158,7 +158,7 @@ export default function Home() {
- , + message: 'Success!', + }, + [STATUS_ERROR]: { + icon: , + message: 'An error occurred.', + }, + [STATUS_INVALID]: { + icon: , + message: 'An error occurred.', + }, + [STATUS_PROCESSING]: { + icon: , + message: '', + }, + [STATUS_WARNING]: { + icon: , + 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, + }; +}