diff --git a/pkg/webui/components/form/context.js b/pkg/webui/components/form/context.js deleted file mode 100644 index 86ca8e4ae1..0000000000 --- a/pkg/webui/components/form/context.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' - -export default React.createContext() diff --git a/pkg/webui/components/form/field/error.js b/pkg/webui/components/form/field/error.js new file mode 100644 index 0000000000..4ed76f4e4b --- /dev/null +++ b/pkg/webui/components/form/field/error.js @@ -0,0 +1,70 @@ +// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import classNames from 'classnames' +import React from 'react' + +import Icon from '@ttn-lw/components/icon' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import style from './field.styl' + +const FieldError = ({ content, error, warning, title, className, id }) => { + const icon = error ? 'error' : 'warning' + const contentValues = content.values || {} + const classname = classNames(style.message, className, { + [style.show]: content && content !== '', + [style.hide]: !content || content === '', + [style.err]: error, + [style.warn]: warning, + }) + + if (title) { + contentValues.field = + } + + return ( +
+ + +
+ ) +} + +FieldError.propTypes = { + className: PropTypes.string, + content: PropTypes.oneOfType([ + PropTypes.error, + PropTypes.shape({ + message: PropTypes.error.isRequired, + values: PropTypes.shape({}).isRequired, + }), + ]).isRequired, + error: PropTypes.bool, + id: PropTypes.string.isRequired, + title: PropTypes.message, + warning: PropTypes.bool, +} + +FieldError.defaultProps = { + className: undefined, + title: undefined, + warning: false, + error: false, +} + +export default FieldError diff --git a/pkg/webui/components/form/field/index.js b/pkg/webui/components/form/field/index.js index 6bd7f0c22e..ceaf099276 100644 --- a/pkg/webui/components/form/field/index.js +++ b/pkg/webui/components/form/field/index.js @@ -1,4 +1,4 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,21 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React from 'react' -import bind from 'autobind-decorator' +import React, { useCallback } from 'react' import classnames from 'classnames' -import { getIn } from 'formik' - -import Icon from '@ttn-lw/components/icon' +import { useField } from 'formik' +import { isPlainObject } from 'lodash' import Message from '@ttn-lw/lib/components/message' import from from '@ttn-lw/lib/from' import PropTypes from '@ttn-lw/lib/prop-types' -import FormContext from '../context' +import { useFormContext } from '..' import Tooltip from './tooltip' +import FieldError from './error' import style from './field.styl' @@ -56,284 +55,228 @@ const isValueEmpty = value => { return false } -class FormField extends React.Component { - static contextType = FormContext - - static propTypes = { - className: PropTypes.string, - component: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.string, - PropTypes.shape({ - render: PropTypes.func.isRequired, - }), - ]).isRequired, - decode: PropTypes.func, - description: PropTypes.message, - disabled: PropTypes.bool, - encode: PropTypes.func, - fieldWidth: PropTypes.oneOf([ - 'xxs', - 'xs', - 's', - 'm', - 'l', - 'xl', - 'xxl', - 'full', - 'half', - 'third', - 'quarter', - ]), - name: PropTypes.string.isRequired, - onBlur: PropTypes.func, - onChange: PropTypes.func, - readOnly: PropTypes.bool, - required: PropTypes.bool, - title: PropTypes.message, - titleChildren: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), - tooltipId: PropTypes.string, - warning: PropTypes.message, - } - - static defaultProps = { - className: undefined, - disabled: false, - encode: value => value, - decode: value => value, - fieldWidth: undefined, - onChange: () => null, - onBlur: () => null, - warning: '', - description: '', - readOnly: false, - required: false, - title: undefined, - titleChildren: null, - tooltipId: '', +const extractValue = value => { + let newValue = value + if (typeof value === 'object' && value !== null && 'target' in value) { + const target = value.target + if ('type' in target && target.type === 'checkbox') { + newValue = target.checked + } else if ('value' in target) { + newValue = target.value + } } - componentDidMount() { - const { name } = this.props + return newValue +} - this.context.registerField(name, this) - } +const FormField = props => { + const { + className, + component: Component, + decode, + description, + disabled: inputDisabled, + encode, + fieldWidth, + name, + readOnly, + required, + title, + titleChildren, + tooltipId, + warning, + validate, + onChange, + onBlur, + } = props + + const { + disabled: formDisabled, + validateOnBlur, + setFieldValue, + setFieldTouched, + } = useFormContext() + + // Initialize field, which also takes care of registering fields in formik's internal registry. + const [{ value: encodedValue }, { touched, error = false }] = useField({ + name, + validate, + }) - componentWillUnmount() { - const { name } = this.props + const handleChange = useCallback( + async (value, enforceValidation = false) => { + const newValue = encode(extractValue(value)) + let isSyntheticEvent = false - this.context.unregisterField(name) - } + if (isPlainObject(value)) { + // Check if the value is react's synthetic event. + isSyntheticEvent = 'target' in value - extractValue(value) { - let newValue = value - if (typeof value === 'object' && value !== null && 'target' in value) { - const target = value.target - if ('type' in target && target.type === 'checkbox') { - newValue = target.checked - } else if ('value' in target) { - newValue = target.value + // TODO: Remove `await` and event persist when https://github.com/jaredpalmer/formik/issues/2457 + // is resolved. + if (typeof value.persist === 'function') { + value.persist() + } } - } - - return newValue - } - @bind - async handleChange(value, enforceValidation = false) { - const { name, onChange, encode } = this.props - const { setFieldValue, setFieldTouched } = this.context + await setFieldValue(name, newValue) - const newValue = encode(this.extractValue(value)) - let isSyntheticEvent = false - - if (typeof value === 'object' && value !== null) { - // Check if the value is react's synthetic event. - isSyntheticEvent = 'target' in value - - // TODO: Remove `await` and event persist when https://github.com/jaredpalmer/formik/issues/2457 - // is resolved. - if (typeof value.persist === 'function') { - value.persist() + if (enforceValidation) { + setFieldTouched(name, true, true) } - } - - await setFieldValue(name, newValue) - - if (enforceValidation) { - setFieldTouched(name) - } - onChange(isSyntheticEvent ? value : encode(value)) - } + onChange(isSyntheticEvent ? value : encode(value)) + }, + [encode, name, onChange, setFieldTouched, setFieldValue], + ) - @bind - handleBlur(event) { - const { name, onBlur } = this.props - const { validateOnBlur, setFieldTouched } = this.context + const handleBlur = useCallback( + event => { + if (validateOnBlur) { + const value = extractValue(event) + setFieldTouched(name, !isValueEmpty(value)) + } - if (validateOnBlur) { - const value = this.extractValue(event) - setFieldTouched(name, !isValueEmpty(value)) - } + onBlur(event) + }, + [validateOnBlur, onBlur, setFieldTouched, name], + ) - onBlur(event) + const value = decode(encodedValue) + const disabled = inputDisabled || formDisabled + const hasTooltip = Boolean(tooltipId) + const hasTitle = Boolean(title) + const showError = touched && Boolean(error) + const showWarning = !Boolean(error) && Boolean(warning) + const showDescription = !showError && !showWarning && Boolean(description) + const tooltipIcon = hasTooltip ? : null + const describedBy = showError + ? `${name}-field-error` + : showWarning + ? `${name}-field-warning` + : showDescription + ? `${name}-field-description` + : undefined + + const fieldMessage = showError ? ( +
+ +
+ ) : showWarning ? ( +
+ +
+ ) : showDescription ? ( + + ) : null + + const fieldComponentProps = { + value, + error: showError, + warning: showWarning, + name, + id: name, + disabled, + onChange: handleChange, + onBlur: handleBlur, + readOnly, } - render() { - const { - className, - decode, - fieldWidth, - name, - title, - titleChildren, - warning, - description, - disabled, - required, - readOnly, - tooltipId, - component: Component, - } = this.props - const { disabled: formDisabled } = this.context - - const fieldValue = decode(getIn(this.context.values, name)) - const fieldError = getIn(this.context.errors, name) - const fieldTouched = getIn(this.context.touched, name) || false - const fieldDisabled = disabled || formDisabled - - const hasError = Boolean(fieldError) - const hasWarning = Boolean(warning) - const hasDescription = Boolean(description) - const hasTooltip = Boolean(tooltipId) - const hasTitle = Boolean(title) - - const showError = fieldTouched && hasError - const showWarning = !hasError && hasWarning - const showDescription = !showError && !showWarning && hasDescription - - const describedBy = showError - ? `${name}-field-error` - : showWarning - ? `${name}-field-warning` - : showDescription - ? `${name}-field-description` - : undefined - - const fieldMessage = showError ? ( -
- -
- ) : showWarning ? ( -
- -
- ) : showDescription ? ( - - ) : null - - let tooltipIcon = null - if (hasTooltip) { - tooltipIcon = - } - - const fieldComponentProps = { - value: fieldValue, + const cls = classnames( + className, + style.field, + from(style, { error: showError, warning: showWarning, - name, - id: name, - disabled: fieldDisabled, - onChange: this.handleChange, - onBlur: this.handleBlur, + [`field-width-${fieldWidth}`]: Boolean(fieldWidth), + required, readOnly, - } + hasTooltip, + }), + ) - const cls = classnames( - className, - style.field, - from(style, { - error: showError, - warning: showWarning, - [`field-width-${fieldWidth}`]: Boolean(fieldWidth), - required, - readOnly, - hasTooltip, - }), - ) - - return ( -
- {hasTitle && ( -
- - {tooltipIcon} - {titleChildren} -
- )} -
- + {hasTitle && ( +
+ - {fieldMessage} + {tooltipIcon} + {titleChildren}
+ )} +
+ + {fieldMessage}
- ) - } -} - -const Err = ({ content, error, warning, title, className, id }) => { - const icon = error ? 'error' : 'warning' - const contentValues = content.values || {} - const classname = classnames(style.message, className, { - [style.show]: content && content !== '', - [style.hide]: !content || content === '', - [style.err]: error, - [style.warn]: warning, - }) - - if (title) { - contentValues.field = - } - - return ( -
- -
) } -Err.propTypes = { +FormField.propTypes = { className: PropTypes.string, - content: PropTypes.oneOfType([ - PropTypes.error, + component: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, PropTypes.shape({ - message: PropTypes.error.isRequired, - values: PropTypes.shape({}).isRequired, + render: PropTypes.func.isRequired, }), ]).isRequired, - error: PropTypes.bool, - id: PropTypes.string.isRequired, + decode: PropTypes.func, + description: PropTypes.message, + disabled: PropTypes.bool, + encode: PropTypes.func, + fieldWidth: PropTypes.oneOf([ + 'xxs', + 'xs', + 's', + 'm', + 'l', + 'xl', + 'xxl', + 'full', + 'half', + 'third', + 'quarter', + ]), + name: PropTypes.string.isRequired, + onBlur: PropTypes.func, + onChange: PropTypes.func, + readOnly: PropTypes.bool, + required: PropTypes.bool, title: PropTypes.message, - warning: PropTypes.bool, + titleChildren: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), + tooltipId: PropTypes.string, + validate: PropTypes.func, + warning: PropTypes.message, } -Err.defaultProps = { +FormField.defaultProps = { className: undefined, + decode: value => value, + description: '', + disabled: false, + encode: value => value, + fieldWidth: undefined, + onBlur: () => null, + onChange: () => null, + readOnly: false, + required: false, title: undefined, - warning: false, - error: false, + titleChildren: null, + tooltipId: '', + validate: undefined, + warning: '', } export default FormField diff --git a/pkg/webui/components/form/field/info.js b/pkg/webui/components/form/field/info.js index a9459180cb..964648e06a 100644 --- a/pkg/webui/components/form/field/info.js +++ b/pkg/webui/components/form/field/info.js @@ -20,48 +20,44 @@ import Message from '@ttn-lw/lib/components/message' import PropTypes from '@ttn-lw/lib/prop-types' import from from '@ttn-lw/lib/from' -import FormContext from '../context' +import { useFormContext } from '..' import Tooltip from './tooltip' import style from './field.styl' -class InfoField extends React.Component { - static contextType = FormContext - - static propTypes = { - children: PropTypes.node.isRequired, - className: PropTypes.string, - disabled: PropTypes.bool, - title: PropTypes.message, - tooltipId: PropTypes.string, - } - - static defaultProps = { - className: undefined, - title: undefined, - disabled: false, - tooltipId: undefined, - } +const InfoField = props => { + const { children, className, title, disabled: fieldDisabled, tooltipId } = props + const { disabled: formDisabled } = useFormContext() + const disabled = formDisabled || fieldDisabled + const cls = classnames(className, style.field, from(style, { disabled })) + + return ( +
+ {title && ( +
+ + {tooltipId && } +
+ )} +
{children}
+
+ ) +} - render() { - const { children, className, title, disabled: fieldDisabled, tooltipId } = this.props - const { disabled: formDisabled } = this.context - const disabled = formDisabled || fieldDisabled - const cls = classnames(className, style.field, from(style, { disabled })) +InfoField.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, + disabled: PropTypes.bool, + title: PropTypes.message, + tooltipId: PropTypes.string, +} - return ( -
- {title && ( -
- - {tooltipId && } -
- )} -
{children}
-
- ) - } +InfoField.defaultProps = { + className: undefined, + title: undefined, + disabled: false, + tooltipId: undefined, } export default InfoField diff --git a/pkg/webui/components/form/index.js b/pkg/webui/components/form/index.js index 6f53d3e622..c1022329c9 100644 --- a/pkg/webui/components/form/index.js +++ b/pkg/webui/components/form/index.js @@ -12,12 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -/* eslint-disable react/sort-prop-types */ -import React from 'react' -import { Formik, yupToFormErrors, useFormikContext, validateYupSchema } from 'formik' -import bind from 'autobind-decorator' +import React, { useCallback, useEffect } from 'react' +import { + yupToFormErrors, + useFormikContext, + validateYupSchema, + useFormik, + FormikProvider, +} from 'formik' import scrollIntoView from 'scroll-into-view-if-needed' import { defineMessages } from 'react-intl' +import { isPlainObject } from 'lodash' import Notification from '@ttn-lw/components/notification' import ErrorNotification from '@ttn-lw/components/error-notification' @@ -25,7 +30,6 @@ import ErrorNotification from '@ttn-lw/components/error-notification' import PropTypes from '@ttn-lw/lib/prop-types' import { ingestError } from '@ttn-lw/lib/errors/utils' -import FormContext from './context' import FormField from './field' import FormInfoField from './field/info' import FormSubmit from './submit' @@ -37,224 +41,191 @@ const m = defineMessages({ submitFailed: 'Submit failed', }) -class InnerForm extends React.PureComponent { - static propTypes = { - children: PropTypes.node.isRequired, - className: PropTypes.string, - id: PropTypes.string, - formError: PropTypes.error, - formErrorTitle: PropTypes.message, - formInfo: PropTypes.message, - formInfoTitle: PropTypes.message, - handleSubmit: PropTypes.func.isRequired, - isSubmitting: PropTypes.bool.isRequired, - isValid: PropTypes.bool.isRequired, - } +const Form = props => { + const { + children, + className, + disabled, + enableReinitialize, + error, + errorTitle, + formikRef, + id, + info, + infoTitle, + initialValues, + onReset, + onSubmit, + validateOnBlur, + validateOnChange, + validateOnMount, + validateSync, + validationContext, + validationSchema, + } = props + + const notificationRef = React.useRef() + + // Recreate the validation hook to allow passing down validation contexts. + const validate = useCallback( + values => { + if (!validationSchema) { + return {} + } - static defaultProps = { - className: undefined, - id: undefined, - formInfo: undefined, - formInfoTitle: undefined, - formError: undefined, - formErrorTitle: m.submitFailed, - } + if (validateSync) { + try { + validateYupSchema(values, validationSchema, validateSync, validationContext) - constructor(props) { - super(props) - this.notificationRef = React.createRef() - } + return {} + } catch (err) { + if (err.name === 'ValidationError') { + return yupToFormErrors(err) + } - componentDidUpdate(prevProps) { - const { formError, isSubmitting, isValid } = this.props - const { isSubmitting: prevIsSubmitting, formError: prevFormError } = prevProps + throw error + } + } + + return new Promise((resolve, reject) => { + validateYupSchema(values, validationSchema, validateSync, validationContext).then( + () => { + resolve({}) + }, + err => { + // Resolve yup errors, see https://jaredpalmer.com/formik/docs/migrating-v2#validate. + if (err.name === 'ValidationError') { + resolve(yupToFormErrors(err)) + } else { + // Throw any other errors as it is not related to the validation process. + reject(err) + } + }, + ) + }) + }, + [validationSchema, validateSync, validationContext, error], + ) + + const handleSubmit = useCallback( + async (...args) => { + try { + return await onSubmit(...args) + } catch (error) { + // Make sure all unhandled exceptions during submit are ingested. + ingestError(error, { ingestedBy: 'FormSubmit' }) + + throw error + } + }, + [onSubmit], + ) + + // Initialize formik and get the formik context to provide to form children. + const formik = useFormik({ + initialValues, + validate, + onSubmit: handleSubmit, + onReset, + validateOnMount, + validateOnBlur, + validateSync, + validateOnChange, + enableReinitialize, + }) + + const { + isSubmitting, + isValid, + handleSubmit: handleFormikSubmit, + handleReset: handleFormikReset, + } = formik + + // Connect the ref with the formik context to ensure compatibility with older form components. + // NOTE: New components should not use the ref, but use the form context directly. + // TODO: Remove this once all forms have been refactored to use context. + if (isPlainObject(formikRef) && 'current' in formikRef) { + formikRef.current = formik + } + useEffect(() => { // Scroll form notification into view if needed. - if (formError && !prevFormError) { - scrollIntoView(this.notificationRef.current, { behavior: 'smooth' }) - this.notificationRef.current.focus({ preventScroll: true }) + if (error) { + scrollIntoView(notificationRef.current, { behavior: 'smooth' }) + notificationRef.current.focus({ preventScroll: true }) } // Scroll invalid fields into view if needed and focus them. - if (prevIsSubmitting && !isSubmitting && !isValid) { + if (!isSubmitting && !isValid) { const firstErrorNode = document.querySelectorAll('[data-needs-focus="true"]')[0] if (firstErrorNode) { scrollIntoView(firstErrorNode, { behavior: 'smooth' }) firstErrorNode.querySelector('input,textarea,canvas,video').focus({ preventScroll: true }) } } - } - - render() { - const { - className, - children, - formError, - formErrorTitle, - formInfo, - formInfoTitle, - handleSubmit, - id, - ...rest - } = this.props - - return ( -
- {(formError || formInfo) && ( -
- {formError && } - {formInfo && } + }, [error, isSubmitting, isValid]) + + return ( + + + {(error || info) && ( +
+ {error && } + {info && }
)} - - {children} - + {children} - ) - } +
+ ) } -const formRenderer = - ({ children, ...rest }) => - renderProps => { - const { className, error, errorTitle, info, infoTitle, disabled, id } = rest - const { handleSubmit, ...restFormikProps } = renderProps - - return ( - - {children} - - ) - } - -class Form extends React.PureComponent { - static propTypes = { - enableReinitialize: PropTypes.bool, - formikRef: PropTypes.shape({ current: PropTypes.shape({}) }), - initialValues: PropTypes.shape({}), - onReset: PropTypes.func, - onSubmit: PropTypes.func, - validateOnMount: PropTypes.bool, - validateOnBlur: PropTypes.bool, - validateOnChange: PropTypes.bool, - validationSchema: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.func]), - validationContext: PropTypes.shape({}), - validateSync: PropTypes.bool, - } - - static defaultProps = { - enableReinitialize: false, - formikRef: undefined, - initialValues: undefined, - onReset: () => null, - validateOnBlur: true, - validateOnMount: false, - validateOnChange: false, - validationSchema: undefined, - validationContext: {}, - validateSync: true, - onSubmit: () => null, - } - - @bind - async handleSubmit(...args) { - const { onSubmit } = this.props - - try { - return await onSubmit(...args) - } catch (error) { - // Make sure all unhandled exceptions during submit are ingested. - ingestError(error, { ingestedBy: 'FormSubmit' }) - - throw error - } - } - - @bind - validate(values) { - const { validationSchema, validationContext, validateSync } = this.props - - if (!validationSchema) { - return {} - } - - if (validateSync) { - try { - validateYupSchema(values, validationSchema, validateSync, validationContext) - - return {} - } catch (error) { - if (error.name === 'ValidationError') { - return yupToFormErrors(error) - } - - throw error - } - } - - return new Promise((resolve, reject) => { - validateYupSchema(values, validationSchema, validateSync, validationContext).then( - () => { - resolve({}) - }, - error => { - // Resolve yup errors, see https://jaredpalmer.com/formik/docs/migrating-v2#validate. - if (error.name === 'ValidationError') { - resolve(yupToFormErrors(error)) - } else { - // Throw any other errors as it is not related to the validation process. - reject(error) - } - }, - ) - }) - } - - render() { - const { - onReset, - initialValues, - validateOnBlur, - validateOnChange, - validationSchema, - validationContext, - validateOnMount, - formikRef, - enableReinitialize, - ...rest - } = this.props +Form.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, + disabled: PropTypes.bool, + enableReinitialize: PropTypes.bool, + error: PropTypes.error, + errorTitle: PropTypes.message, + formikRef: PropTypes.shape({ current: PropTypes.shape({}) }), + id: PropTypes.string, + info: PropTypes.message, + infoTitle: PropTypes.message, + initialValues: PropTypes.shape({}), + onReset: PropTypes.func, + onSubmit: PropTypes.func, + validateOnBlur: PropTypes.bool, + validateOnChange: PropTypes.bool, + validateOnMount: PropTypes.bool, + validateSync: PropTypes.bool, + validationContext: PropTypes.shape({}), + validationSchema: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.func]), +} - return ( - - {formRenderer(rest)} - - ) - } +Form.defaultProps = { + className: undefined, + disabled: false, + enableReinitialize: false, + error: undefined, + errorTitle: m.submitFailed, + info: undefined, + infoTitle: undefined, + formikRef: undefined, + id: undefined, + initialValues: undefined, + onReset: () => null, + onSubmit: () => null, + validateOnBlur: true, + validateOnChange: false, + validateOnMount: false, + validateSync: true, + validationContext: {}, + validationSchema: undefined, } Form.Field = FormField diff --git a/pkg/webui/components/form/section/index.js b/pkg/webui/components/form/section/index.js index ba04122be4..879aabe88d 100644 --- a/pkg/webui/components/form/section/index.js +++ b/pkg/webui/components/form/section/index.js @@ -16,13 +16,14 @@ import React from 'react' import classnames from 'classnames' import { defineMessages, useIntl } from 'react-intl' -import { useFormContext } from '@ttn-lw/components/form' import Icon from '@ttn-lw/components/icon' import Message from '@ttn-lw/lib/components/message' import PropTypes from '@ttn-lw/lib/prop-types' +import { useFormContext } from '..' + import style from './section.styl' const m = defineMessages({ diff --git a/pkg/webui/components/form/submit/index.js b/pkg/webui/components/form/submit/index.js index a7a30db52c..ed8bf55efe 100644 --- a/pkg/webui/components/form/submit/index.js +++ b/pkg/webui/components/form/submit/index.js @@ -16,37 +16,34 @@ import React from 'react' import PropTypes from '@ttn-lw/lib/prop-types' -import FormContext from '../context' - -class FormSubmit extends React.Component { - static contextType = FormContext - - static propTypes = { - component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - disabled: PropTypes.bool, +import { useFormContext } from '..' + +const FormSubmit = props => { + const { component: Component, disabled, ...rest } = props + const formContext = useFormContext() + + const submitProps = { + isValid: formContext.isValid, + isSubmitting: formContext.isSubmitting, + isValidating: formContext.isValidating, + submitCount: formContext.submitCount, + dirty: formContext.dirty, + validateForm: formContext.validateForm, + validateField: formContext.validateField, + disabled: formContext.disabled || disabled, } - static defaultProps = { - component: 'button', - disabled: false, - } + return +} - render() { - const { component: Component, disabled, ...rest } = this.props - - const submitProps = { - isValid: this.context.isValid, - isSubmitting: this.context.isSubmitting, - isValidating: this.context.isValidating, - submitCount: this.context.submitCount, - dirty: this.context.dirty, - validateForm: this.context.validateForm, - validateField: this.context.validateField, - disabled: this.context.disabled || disabled, - } - - return - } +FormSubmit.propTypes = { + component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + disabled: PropTypes.bool, +} + +FormSubmit.defaultProps = { + component: 'button', + disabled: false, } export default FormSubmit diff --git a/pkg/webui/console/components/webhook-template-form/index.js b/pkg/webui/console/components/webhook-template-form/index.js index e594e5a478..bed01ea334 100644 --- a/pkg/webui/console/components/webhook-template-form/index.js +++ b/pkg/webui/console/components/webhook-template-form/index.js @@ -130,7 +130,6 @@ export default class WebhookTemplateForm extends Component { validationSchema={validationSchema} initialValues={initialValues} error={error} - formikRef={this.form} >