123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- import {Component} from 'react';
- import styled from '@emotion/styled';
- import isEqual from 'lodash/isEqual';
- import Button from 'sentry/components/button';
- import FormContext, {
- FormContextData,
- } from 'sentry/components/deprecatedforms/formContext';
- import FormState from 'sentry/components/forms/state';
- import {t} from 'sentry/locale';
- type FormProps = {
- cancelLabel?: string;
- className?: string;
- errorMessage?: React.ReactNode;
- extraButton?: React.ReactNode;
- footerClass?: string;
- hideErrors?: boolean;
- initialData?: object;
- onCancel?: () => void;
- onSubmit?: (
- data: object,
- onSubmitSuccess: (data: object) => void,
- onSubmitError: (error: object) => void
- ) => void;
- onSubmitError?: (error: object) => void;
- onSubmitSuccess?: (data: object) => void;
- requireChanges?: boolean;
- resetOnError?: boolean;
- submitDisabled?: boolean;
- submitLabel?: string;
- };
- type FormClassState = {
- data: any;
- errors: {non_field_errors?: object[]} & object;
- initialData: object;
- state: FormState;
- };
- // Re-export for compatibility alias.
- export type Context = FormContextData;
- class Form<
- Props extends FormProps = FormProps,
- State extends FormClassState = FormClassState
- > extends Component<Props, State> {
- static defaultProps = {
- cancelLabel: t('Cancel'),
- submitLabel: t('Save Changes'),
- submitDisabled: false,
- footerClass: 'form-actions align-right',
- className: 'form-stacked',
- requireChanges: false,
- hideErrors: false,
- resetOnError: false,
- errorMessage: t(
- 'Unable to save your changes. Please ensure all fields are valid and try again.'
- ),
- };
- constructor(props: Props, context: Context) {
- super(props, context);
- this.state = {
- data: {...this.props.initialData},
- errors: {},
- initialData: {...this.props.initialData},
- state: FormState.READY,
- } as State;
- }
- getContext() {
- const {data, errors} = this.state;
- return {
- form: {
- data,
- errors,
- onFieldChange: this.onFieldChange,
- },
- };
- }
- onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
- e.preventDefault();
- if (!this.props.onSubmit) {
- throw new Error('onSubmit is a required prop');
- }
- this.props.onSubmit(this.state.data, this.onSubmitSuccess, this.onSubmitError);
- };
- onSubmitSuccess = (data: object) => {
- this.setState({
- state: FormState.READY,
- errors: {},
- initialData: {...this.state.data, ...(data || {})},
- });
- this.props.onSubmitSuccess && this.props.onSubmitSuccess(data);
- };
- onSubmitError = error => {
- this.setState({
- state: FormState.ERROR,
- errors: error.responseJSON,
- });
- if (this.props.resetOnError) {
- this.setState({
- initialData: {},
- });
- }
- this.props.onSubmitError && this.props.onSubmitError(error);
- };
- onFieldChange = (name: string, value: string | number) => {
- this.setState(state => ({
- data: {
- ...state.data,
- [name]: value,
- },
- }));
- };
- render() {
- const isSaving = this.state.state === FormState.SAVING;
- const {initialData, data} = this.state;
- const {errorMessage, hideErrors, requireChanges} = this.props;
- const hasChanges = requireChanges
- ? Object.keys(data).length && !isEqual(data, initialData)
- : true;
- const isError = this.state.state === FormState.ERROR;
- const nonFieldErrors = this.state.errors && this.state.errors.non_field_errors;
- return (
- <FormContext.Provider value={this.getContext()}>
- <StyledForm
- onSubmit={this.onSubmit}
- className={this.props.className}
- aria-label={this.props['aria-label']}
- >
- {isError && !hideErrors && (
- <div className="alert alert-error alert-block">
- {nonFieldErrors ? (
- <div>
- <p>
- {t(
- 'Unable to save your changes. Please correct the following errors try again.'
- )}
- </p>
- <ul>
- {nonFieldErrors.map((e, i) => (
- <li key={i}>{e}</li>
- ))}
- </ul>
- </div>
- ) : (
- errorMessage
- )}
- </div>
- )}
- {this.props.children}
- <div className={this.props.footerClass} style={{marginTop: 25}}>
- <Button
- priority="primary"
- disabled={isSaving || this.props.submitDisabled || !hasChanges}
- type="submit"
- aria-label={this.props.submitLabel ?? t('Submit')}
- >
- {this.props.submitLabel}
- </Button>
- {this.props.onCancel && (
- <Button
- type="button"
- disabled={isSaving}
- onClick={this.props.onCancel}
- style={{marginLeft: 5}}
- aria-label={this.props.cancelLabel ?? t('Cancel')}
- >
- {this.props.cancelLabel}
- </Button>
- )}
- {this.props.extraButton}
- </div>
- </StyledForm>
- </FormContext.Provider>
- );
- }
- }
- // Note: this is so we can use this as a selector for SelectField
- // We need to keep `Form` as a React Component because ApiForm extends it :/
- export const StyledForm = styled('form')``;
- export default Form;
|