Merge pull request #1697 from merico-dev/jc-1606-add-edit-connection-validation-onfocus
`feat` `v0.10.0` connection inputs: add validate-on-focus support
diff --git a/config-ui/src/components/validation/InputValidationError.jsx b/config-ui/src/components/validation/InputValidationError.jsx
index 3eb8f57..0b90b9b 100644
--- a/config-ui/src/components/validation/InputValidationError.jsx
+++ b/config-ui/src/components/validation/InputValidationError.jsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useEffect, useState, useCallback } from 'react'
import {
Colors,
Icon,
@@ -9,7 +9,71 @@
} from '@blueprintjs/core'
const InputValidationError = (props) => {
- const { error, position = Position.TOP } = props
+ const {
+ error,
+ position = Position.TOP,
+ // eslint-disable-next-line no-unused-vars
+ validateOnFocus = false,
+ elementRef, onError = () => {},
+ onSuccess = () => {},
+ interactionKind = PopoverInteractionKind.HOVER_TARGET_ONLY
+ } = props
+
+ const [elementIsFocused, setElementIsFocused] = useState(false)
+ // eslint-disable-next-line no-unused-vars
+ const [inputElement, setInputElement] = useState(null)
+
+ const handleElementFocus = useCallback((isFocused, ref) => {
+ setElementIsFocused(isFocused)
+ if (error) {
+ elementRef?.current.parentElement.classList.remove('valid-field')
+ elementRef?.current.parentElement.classList.add('invalid-field')
+ } else {
+ elementRef?.current.parentElement.classList.remove('invalid-field')
+ elementRef?.current.parentElement.classList.add('valid-field')
+ }
+ }, [elementRef, error])
+
+ const handleElementBlur = useCallback((isFocused, ref) => {
+ setElementIsFocused(isFocused)
+ if (!error) {
+ elementRef?.current.parentElement.classList.remove('invalid-field')
+ }
+ }, [elementRef, error])
+
+ useEffect(() => {
+ const iRef = elementRef?.current
+ if (iRef) {
+ setInputElement(iRef)
+ iRef.addEventListener('focus', (e) => handleElementFocus(true, iRef), true)
+ iRef.addEventListener('keyup', (e) => handleElementFocus(true, iRef), true)
+ iRef.addEventListener('blur', (e) => handleElementBlur(false, iRef), true)
+ } else {
+ setInputElement(null)
+ }
+
+ return () => {
+ iRef?.removeEventListener('focus', setElementIsFocused, true)
+ iRef?.removeEventListener('keyup', setElementIsFocused, true)
+ iRef?.removeEventListener('blur', setElementIsFocused, true)
+ setInputElement(null)
+ }
+ }, [elementRef, handleElementBlur, handleElementFocus])
+
+ useEffect(() => {
+ if (error && validateOnFocus && elementIsFocused) {
+ onError(elementRef?.current?.id ? elementRef?.current?.id : null)
+ } else if (error && !validateOnFocus) {
+ onError(elementRef?.current?.id ? elementRef?.current?.id : null)
+ } else {
+ onSuccess()
+ }
+ }, [error, onError, onSuccess, elementIsFocused, validateOnFocus, elementRef])
+
+ useEffect(() => {
+
+ }, [validateOnFocus])
+
return error
? (
<div className='inline-input-error' style={{ outline: 'none', cursor: 'pointer', margin: '5px 5px 3px 5px' }}>
@@ -18,9 +82,17 @@
usePortal={true}
openOnTargetFocus={true}
intent={Intent.WARNING}
- interactionKind={PopoverInteractionKind.HOVER_TARGET_ONLY}
+ interactionKind={interactionKind}
+ enforceFocus={false}
+ // autoFocus={false}
>
- <Icon icon='warning-sign' size={12} color={Colors.RED5} style={{ outline: 'none' }} />
+ <Icon
+ icon='warning-sign'
+ size={12}
+ color={(validateOnFocus && elementIsFocused) || (error && !validateOnFocus) ? Colors.RED5 : Colors.GRAY5}
+ style={{ outline: 'none' }}
+ onClick={(e) => e.stopPropagation()}
+ />
<div style={{ outline: 'none', padding: '5px', borderTop: `2px solid ${Colors.RED5}` }}>{error}</div>
</Popover>
</div>
diff --git a/config-ui/src/pages/configure/connections/ConnectionForm.jsx b/config-ui/src/pages/configure/connections/ConnectionForm.jsx
index 2ec5961..f16fdc8 100644
--- a/config-ui/src/pages/configure/connections/ConnectionForm.jsx
+++ b/config-ui/src/pages/configure/connections/ConnectionForm.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useCallback } from 'react'
+import React, { useEffect, useState, useCallback, useRef } from 'react'
import {
Button, Colors,
FormGroup, InputGroup, Label,
@@ -54,9 +54,15 @@
placeholders
} = props
+ const connectionNameRef = useRef()
+ const connectionEndpointRef = useRef()
+ const connectionTokenRef = useRef()
+
// const [isValidForm, setIsValidForm] = useState(true)
const [allowedAuthTypes, setAllowedAuthTypes] = useState(['token', 'plain'])
const [showTokenCreator, setShowTokenCreator] = useState(false)
+ const [stateErrored, setStateErrored] = useState(false)
+
const getConnectionStatusIcon = () => {
let statusIcon = <Icon icon='full-circle' size='10' color={Colors.RED3} />
switch (testStatus) {
@@ -96,6 +102,10 @@
return validationErrors.find(e => e.includes(fieldId))
}
+ const activateErrorStates = (elementId) => {
+ setStateErrored(elementId || false)
+ }
+
useEffect(() => {
if (!allowedAuthTypes.includes(authType)) {
console.log('INVALID AUTH TYPE!')
@@ -184,17 +194,24 @@
</Label>
<InputGroup
id='connection-name'
+ inputRef={connectionNameRef}
disabled={isTesting || isSaving || isLocked}
readOnly={[Providers.GITHUB, Providers.GITLAB, Providers.JENKINS].includes(activeProvider.id)}
placeholder={placeholders ? placeholders.name : 'Enter Instance Name'}
value={name}
onChange={(e) => onNameChange(e.target.value)}
- className={`input connection-name-input ${fieldHasError('Connection Source') ? 'invalid-field' : ''}`}
+ // className={`input connection-name-input ${fieldHasError('Connection Source') ? 'invalid-field' : ''}`}
+ // className='input connection-name-input'
+ className={`input connection-name-input ${stateErrored === 'connection-name' ? 'invalid-field' : ''}`}
leftIcon={[Providers.GITHUB, Providers.GITLAB, Providers.JENKINS].includes(activeProvider.id) ? 'lock' : null}
inline={true}
rightElement={(
<InputValidationError
error={getFieldError('Connection Source')}
+ elementRef={connectionNameRef}
+ onError={activateErrorStates}
+ onSuccess={() => setStateErrored(null)}
+ validateOnFocus
/>
)}
// fill
@@ -221,15 +238,20 @@
</Label>
<InputGroup
id='connection-endpoint'
+ inputRef={connectionEndpointRef}
disabled={isTesting || isSaving || isLocked}
placeholder={placeholders ? placeholders.endpoint : 'Enter Endpoint URL'}
value={endpointUrl}
onChange={(e) => onEndpointChange(e.target.value)}
- className={`input endpoint-url-input ${fieldHasError('Endpoint') ? 'invalid-field' : ''}`}
+ className={`input endpoint-url-input ${stateErrored === 'connection-endpoint' ? 'invalid-field' : ''}`}
fill
rightElement={(
<InputValidationError
error={getFieldError('Endpoint')}
+ elementRef={connectionEndpointRef}
+ onError={activateErrorStates}
+ onSuccess={() => setStateErrored(null)}
+ validateOnFocus
/>
)}
/>
@@ -257,16 +279,21 @@
</Label>
<InputGroup
id='connection-token'
+ inputRef={connectionTokenRef}
disabled={isTesting || isSaving || isLocked}
placeholder={placeholders ? placeholders.token : 'Enter Auth Token eg. EJrLG8DNeXADQcGOaaaX4B47'}
value={token}
onChange={(e) => onTokenChange(e.target.value)}
- className={`input auth-input ${fieldHasError('Auth') ? 'invalid-field' : ''}`}
+ className={`input auth-input ${stateErrored === 'connection-token' ? 'invalid-field' : ''}`}
fill
required
rightElement={(
<InputValidationError
error={getFieldError('Auth')}
+ elementRef={connectionTokenRef}
+ onError={activateErrorStates}
+ onSuccess={() => setStateErrored(null)}
+ validateOnFocus
/>
)}
/>