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-server-details.tsx b/web/pages/components/config/edit-server-details.tsx index f4593fbb7..d052c7bb1 100644 --- a/web/pages/components/config/edit-server-details.tsx +++ b/web/pages/components/config/edit-server-details.tsx @@ -2,7 +2,7 @@ import React, { useState, useContext, useEffect } from 'react'; import { Button, Tooltip } from 'antd'; import { CopyOutlined, RedoOutlined } from '@ant-design/icons'; -import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield-nosubmit'; +import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield'; import TextFieldWithSubmit from './form-textfield-with-submit'; import { ServerStatusContext } from '../../../utils/server-status-context'; diff --git a/web/pages/components/config/edit-tags.tsx b/web/pages/components/config/edit-tags.tsx index 7b05347b9..d1382bfef 100644 --- a/web/pages/components/config/edit-tags.tsx +++ b/web/pages/components/config/edit-tags.tsx @@ -3,16 +3,18 @@ import React, { useContext, useState, useEffect } from 'react'; import { Typography, Tag, Input } from 'antd'; import { ServerStatusContext } from '../../../utils/server-status-context'; -import { FIELD_PROPS_TAGS, RESET_TIMEOUT, SUCCESS_STATES, postConfigUpdateToAPI } from './constants'; -import TextField from './form-textfield-nosubmit'; +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; export default function EditInstanceTags() { const [newTagInput, setNewTagInput] = useState(''); - const [submitStatus, setSubmitStatus] = useState(null); - const [submitStatusMessage, setSubmitStatusMessage] = useState(''); + const [fieldStatus, setFieldStatus] = useState(null); + // const [submitStatus, setSubmitStatus] = useState(null); + // const [submitStatusMessage, setSubmitStatusMessage] = useState(''); const serverStatusData = useContext(ServerStatusContext); const { serverConfig, setFieldInConfigState } = serverStatusData || {}; @@ -35,36 +37,46 @@ 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 = ({ value }: UpdateArgs) => { - if (submitStatusMessage !== '') { - setSubmitStatusMessage(''); + if (!fieldStatus) { + setFieldStatus(null); } + // if (submitStatusMessage !== '') { + // setSubmitStatusMessage(''); + // } setNewTagInput(value); }; @@ -73,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; } @@ -92,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 (
@@ -113,9 +129,9 @@ 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 index 4d9ef80ad..5044225b3 100644 --- a/web/pages/components/config/form-textfield-with-submit.tsx +++ b/web/pages/components/config/form-textfield-with-submit.tsx @@ -4,7 +4,7 @@ import { Button } from 'antd'; import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants'; import { ServerStatusContext } from '../../../utils/server-status-context'; -import TextField, { TextFieldProps } from './form-textfield-nosubmit'; +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'; diff --git a/web/pages/components/config/form-textfield.tsx b/web/pages/components/config/form-textfield.tsx new file mode 100644 index 000000000..cdb6e6ca1 --- /dev/null +++ b/web/pages/components/config/form-textfield.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { Input, InputNumber } from 'antd'; +import { FieldUpdaterFunc } from '../../../types/config-section'; +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'; // InputNumber +export const TEXTFIELD_TYPE_TEXTAREA = 'textarea'; // Input.TextArea +export const TEXTFIELD_TYPE_URL = 'url'; + +export interface TextFieldProps { + fieldName: string; + + onSubmit?: () => void; + onPressEnter?: () => void; + + className?: string; + disabled?: boolean; + label?: string; + maxLength?: number; + placeholder?: string; + required?: boolean; + status?: StatusState; + tip?: string; + type?: string; + value?: string | number; + onBlur?: FieldUpdaterFunc; + onChange?: FieldUpdaterFunc; +} + + +export default function TextField(props: TextFieldProps) { + const { + className, + disabled, + fieldName, + label, + maxLength, + onBlur, + onChange, + onPressEnter, + placeholder, + required, + status, + tip, + type, + value, + } = props; + + // 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 }); + } + }; + + // 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: any) => { + const val = e.target.value; + if (onBlur) { + onBlur({ value: val }); + } + }; + + 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; + let fieldProps = {}; + if (type === TEXTFIELD_TYPE_TEXTAREA) { + Field = Input.TextArea; + fieldProps = { + autoSize: true, + }; + } else if (type === TEXTFIELD_TYPE_PASSWORD) { + Field = Input.Password; + fieldProps = { + visibilityToggle: true, + }; + } else if (type === TEXTFIELD_TYPE_NUMBER) { + Field = InputNumber; + fieldProps = { + type: 'number', + min: 1, + max: (10**maxLength) - 1, + onKeyDown: (e: React.KeyboardEvent) => { + if (e.target.value.length > maxLength - 1 ) + e.preventDefault(); + return false; + } + }; + } else if (type === TEXTFIELD_TYPE_URL) { + fieldProps = { + type: 'url', + }; + } + + const fieldId = `field-${fieldName}`; + + const { icon: statusIcon, message: statusMessage } = status || {}; + + return ( +
+ { required ? * : null } + +
+ +
+ + { status ? statusMessage : null } + { status ? statusIcon : null } +
+ ); +} + +TextField.defaultProps = { + className: '', + // configPath: '', + disabled: false, + // initialValue: '', + label: '', + maxLength: null, + + placeholder: '', + required: false, + status: null, + tip: '', + type: TEXTFIELD_TYPE_TEXT, + value: '', + onSubmit: () => {}, + onBlur: () => {}, + onChange: () => {}, + onPressEnter: () => {}, +};