mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
import {Component} from 'react';
|
|
import styled from '@emotion/styled';
|
|
import isEqual from 'lodash/isEqual';
|
|
|
|
import {Button} from 'sentry/components/button';
|
|
import type {FormContextData} from 'sentry/components/deprecatedforms/formContext';
|
|
import FormContext 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?.(data);
|
|
};
|
|
|
|
onSubmitError = error => {
|
|
this.setState({
|
|
state: FormState.ERROR,
|
|
errors: error.responseJSON,
|
|
});
|
|
|
|
if (this.props.resetOnError) {
|
|
this.setState({
|
|
initialData: {},
|
|
});
|
|
}
|
|
|
|
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?.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
|
|
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;
|