mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
816 lines
21 KiB
TypeScript
816 lines
21 KiB
TypeScript
import isEqual from 'lodash/isEqual';
|
|
import type {ObservableMap} from 'mobx';
|
|
import {action, computed, makeObservable, observable} from 'mobx';
|
|
|
|
import {addErrorMessage, saveOnBlurUndoMessage} from 'sentry/actionCreators/indicator';
|
|
import type {APIRequestMethod} from 'sentry/api';
|
|
import {Client} from 'sentry/api';
|
|
import FormState from 'sentry/components/forms/state';
|
|
import {t} from 'sentry/locale';
|
|
import type {Choice} from 'sentry/types';
|
|
import {defined} from 'sentry/utils';
|
|
|
|
type Snapshot = Map<string, FieldValue>;
|
|
type SaveSnapshot = (() => number) | null;
|
|
|
|
export type FieldValue = string | number | boolean | Choice | Set<string> | undefined; // is undefined valid here?
|
|
|
|
export type FormOptions = {
|
|
/**
|
|
* Does the form support undo?
|
|
*/
|
|
allowUndo?: boolean;
|
|
/**
|
|
* API endpoint use when saving the form model
|
|
*/
|
|
apiEndpoint?: string;
|
|
/**
|
|
* API method used to save the form model
|
|
*/
|
|
apiMethod?: APIRequestMethod;
|
|
/**
|
|
* Options passed to the API Client
|
|
*/
|
|
apiOptions?: ConstructorParameters<typeof Client>[0];
|
|
/**
|
|
* Initial form data
|
|
*/
|
|
initialData?: Record<string, FieldValue>;
|
|
/**
|
|
* Custom transformer function for use with the error response
|
|
*/
|
|
mapFormErrors?: (responseJson: any) => any;
|
|
/**
|
|
* Callback triggered when a field changes value
|
|
*/
|
|
onFieldChange?: (id: string, finalValue: FieldValue) => void;
|
|
/**
|
|
* Callback triggered when form submission fails
|
|
*/
|
|
onSubmitError?: (error: any, instance: FormModel, id?: string) => void;
|
|
/**
|
|
* Callback triggered when the form is successfully submitted
|
|
*/
|
|
onSubmitSuccess?: (
|
|
response: any,
|
|
instance: FormModel,
|
|
id?: string,
|
|
change?: {new: FieldValue; old: FieldValue}
|
|
) => void;
|
|
/**
|
|
* Should the form reset its state when there are errors after submission.
|
|
*/
|
|
resetOnError?: boolean;
|
|
/**
|
|
* Should fields save individually as they are blurred.
|
|
*/
|
|
saveOnBlur?: boolean;
|
|
/**
|
|
* Custom transformer function used before the API request
|
|
*/
|
|
transformData?: (data: Record<string, any>, instance: FormModel) => Record<string, any>;
|
|
};
|
|
|
|
class FormModel {
|
|
/**
|
|
* Map of field name -> value
|
|
*/
|
|
fields: ObservableMap<string, FieldValue> = observable.map();
|
|
|
|
/**
|
|
* Errors for individual fields
|
|
* Note we don't keep error in `this.fieldState` so that we can easily
|
|
* See if the form is in an "error" state with the `isError` getter
|
|
*/
|
|
errors = new Map();
|
|
|
|
/**
|
|
* State of individual fields
|
|
*
|
|
* Map of field name -> object
|
|
*/
|
|
fieldState = new Map();
|
|
|
|
/**
|
|
* State of the form as a whole
|
|
*/
|
|
formState: FormState | undefined;
|
|
|
|
/**
|
|
* Holds field properties as declared in <Form>
|
|
* Does not need to be observable since these props should never change
|
|
*/
|
|
fieldDescriptor = new Map();
|
|
|
|
/**
|
|
* Holds a list of `fields` states
|
|
*/
|
|
snapshots: Array<Snapshot> = [];
|
|
|
|
/**
|
|
* POJO of field name -> value
|
|
* It holds field values "since last save"
|
|
*/
|
|
initialData: Record<string, FieldValue> = {};
|
|
|
|
api: Client;
|
|
|
|
// TODO(epurkhiser): it looks like this can goa away, along with mapFormErrors,
|
|
// unclear why this is part of the form model
|
|
formErrors: any;
|
|
|
|
options: FormOptions;
|
|
|
|
constructor({initialData, apiOptions, ...options}: FormOptions = {}) {
|
|
makeObservable(this, {
|
|
fields: observable,
|
|
errors: observable,
|
|
fieldState: observable,
|
|
formState: observable,
|
|
|
|
isError: computed,
|
|
isSaving: computed,
|
|
formData: computed,
|
|
formChanged: computed,
|
|
|
|
resetForm: action,
|
|
setFieldDescriptor: action,
|
|
removeField: action,
|
|
setValue: action,
|
|
validateField: action,
|
|
updateShowSaveState: action,
|
|
undo: action,
|
|
saveForm: action,
|
|
saveField: action,
|
|
saveFieldRequest: action,
|
|
handleBlurField: action,
|
|
setFormSaving: action,
|
|
handleSaveField: action,
|
|
handleCancelSaveField: action,
|
|
setFieldState: action,
|
|
setSaving: action,
|
|
setError: action,
|
|
validateForm: action,
|
|
handleErrorResponse: action,
|
|
submitSuccess: action,
|
|
submitError: action,
|
|
});
|
|
|
|
this.options = options ?? {};
|
|
|
|
if (initialData) {
|
|
this.setInitialData(initialData);
|
|
}
|
|
|
|
this.api = new Client(apiOptions);
|
|
}
|
|
|
|
/**
|
|
* Reset state of model
|
|
*/
|
|
reset() {
|
|
this.api.clear();
|
|
this.fieldDescriptor.clear();
|
|
this.resetForm();
|
|
}
|
|
|
|
resetForm() {
|
|
this.fields.clear();
|
|
this.errors.clear();
|
|
this.fieldState.clear();
|
|
this.snapshots = [];
|
|
this.initialData = {};
|
|
}
|
|
|
|
/**
|
|
* Deep equality comparison between last saved state and current fields state
|
|
*/
|
|
get formChanged() {
|
|
return !isEqual(this.initialData, Object.fromEntries(this.fields.toJSON()));
|
|
}
|
|
|
|
get formData() {
|
|
return this.fields;
|
|
}
|
|
|
|
/**
|
|
* Is form saving
|
|
*/
|
|
get isSaving() {
|
|
return this.formState === FormState.SAVING;
|
|
}
|
|
|
|
/**
|
|
* Does form have any errors
|
|
*/
|
|
get isError() {
|
|
return !!this.errors.size;
|
|
}
|
|
|
|
/**
|
|
* Sets initial form data
|
|
*
|
|
* Also resets snapshots
|
|
*/
|
|
setInitialData(initialData?: Record<string, FieldValue>) {
|
|
this.fields.replace(initialData || {});
|
|
this.initialData = Object.fromEntries(this.fields.toJSON()) || {};
|
|
|
|
this.snapshots = [new Map(this.fields.entries())];
|
|
}
|
|
|
|
/**
|
|
* Set form options
|
|
*/
|
|
setFormOptions(options: FormOptions) {
|
|
this.options = {...this.options, ...options} || {};
|
|
}
|
|
|
|
/**
|
|
* Set field properties
|
|
*/
|
|
setFieldDescriptor(id: string, props) {
|
|
// TODO(TS): add type to props
|
|
this.fieldDescriptor.set(id, props);
|
|
|
|
// Set default value iff initialData for field is undefined
|
|
// This must take place before checking for `props.setValue` so that it can
|
|
// be applied to `defaultValue`
|
|
if (
|
|
typeof props.defaultValue !== 'undefined' &&
|
|
typeof this.initialData[id] === 'undefined'
|
|
) {
|
|
this.initialData[id] =
|
|
typeof props.defaultValue === 'function'
|
|
? props.defaultValue()
|
|
: props.defaultValue;
|
|
|
|
this.fields.set(id, this.initialData[id]);
|
|
}
|
|
|
|
if (typeof props.setValue === 'function') {
|
|
this.initialData[id] = props.setValue(this.initialData[id], props);
|
|
this.fields.set(id, this.initialData[id]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a field from the descriptor map and errors.
|
|
*/
|
|
removeField(id: string) {
|
|
this.fields.delete(id);
|
|
this.fieldState.delete(id);
|
|
this.fieldDescriptor.delete(id);
|
|
this.errors.delete(id);
|
|
delete this.initialData[id];
|
|
}
|
|
|
|
/**
|
|
* Creates a cloned Map of `this.fields` and returns a closure that when called
|
|
* will save Map to `snapshots
|
|
*/
|
|
createSnapshot() {
|
|
const snapshot = new Map(this.fields.entries());
|
|
return () => this.snapshots.unshift(snapshot);
|
|
}
|
|
|
|
getDescriptor(id: string, key: string) {
|
|
// Needs to call `has` or else component will not be reactive if `id` doesn't exist in observable map
|
|
const descriptor = this.fieldDescriptor.has(id) && this.fieldDescriptor.get(id);
|
|
if (!descriptor) {
|
|
return null;
|
|
}
|
|
|
|
return descriptor[key];
|
|
}
|
|
|
|
getFieldState(id: string, key: string) {
|
|
// Needs to call `has` or else component will not be reactive if `id` doesn't exist in observable map
|
|
const fieldState = this.fieldState.has(id) && this.fieldState.get(id);
|
|
if (!fieldState) {
|
|
return null;
|
|
}
|
|
|
|
return fieldState[key];
|
|
}
|
|
|
|
getValue<T = FieldValue>(id: string, defaultValue?: T): T {
|
|
if (this.fields.has(id)) {
|
|
return this.fields.get(id) as T;
|
|
}
|
|
|
|
// XXX(epurkhiser): When you don't specify a default value it WILL become a
|
|
// empty string, which is not correctly accounted for in the types. We're
|
|
// doing this for legacy reasons
|
|
return defaultValue ?? ('' as T);
|
|
}
|
|
|
|
getTransformedValue(id: string) {
|
|
const getValue = this.getDescriptor(id, 'getValue');
|
|
const transformer = typeof getValue === 'function' ? getValue : null;
|
|
const value = this.getValue(id);
|
|
|
|
return transformer ? transformer(value) : value;
|
|
}
|
|
|
|
/**
|
|
* Data represented in UI
|
|
*/
|
|
getData() {
|
|
return Object.fromEntries(this.fields.toJSON());
|
|
}
|
|
|
|
/**
|
|
* Form data that will be sent to API endpoint (i.e. after transforms)
|
|
*/
|
|
getTransformedData() {
|
|
const form = this.getData();
|
|
|
|
const data = Object.keys(form)
|
|
.map(id => [id, this.getTransformedValue(id)])
|
|
.reduce<Record<string, any>>((acc, [id, value]) => {
|
|
acc[id] = value;
|
|
return acc;
|
|
}, {});
|
|
|
|
return this.options.transformData ? this.options.transformData(data, this) : data;
|
|
}
|
|
|
|
getError(id: string) {
|
|
// return 'Error Message';
|
|
return this.errors.has(id) && this.errors.get(id);
|
|
}
|
|
|
|
/**
|
|
* Returns true if not required or is required and is not empty
|
|
*/
|
|
isValidRequiredField(id: string) {
|
|
// Check field descriptor to see if field is required
|
|
const isRequired = this.getDescriptor(id, 'required');
|
|
const value = this.getValue(id);
|
|
return !isRequired || (value !== '' && defined(value));
|
|
}
|
|
|
|
isValidField(id: string) {
|
|
return (this.getError(id) || []).length === 0;
|
|
}
|
|
|
|
doApiRequest({
|
|
apiEndpoint,
|
|
apiMethod,
|
|
data,
|
|
}: {
|
|
data: object;
|
|
apiEndpoint?: string;
|
|
apiMethod?: APIRequestMethod;
|
|
}) {
|
|
const endpoint = apiEndpoint || this.options.apiEndpoint || '';
|
|
const method = apiMethod || this.options.apiMethod;
|
|
|
|
return new Promise((resolve, reject) =>
|
|
this.api.request(endpoint, {
|
|
method,
|
|
data,
|
|
success: response => resolve(response),
|
|
error: error => reject(error),
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set the value of the form field
|
|
* if quiet is true, we skip callbacks, validations
|
|
*/
|
|
setValue(id: string, value: FieldValue, {quiet}: {quiet?: boolean} = {}) {
|
|
const transformInput = this.getDescriptor(id, 'transformInput');
|
|
const finalValue =
|
|
typeof transformInput === 'function' ? transformInput(value) : value;
|
|
|
|
this.fields.set(id, finalValue);
|
|
|
|
if (quiet) {
|
|
return;
|
|
}
|
|
|
|
if (this.options.onFieldChange) {
|
|
this.options.onFieldChange(id, finalValue);
|
|
}
|
|
|
|
this.validateField(id);
|
|
this.updateShowSaveState(id, finalValue);
|
|
}
|
|
|
|
validateField(id: string) {
|
|
const validate = this.getDescriptor(id, 'validate');
|
|
let errors: any[] = [];
|
|
|
|
if (typeof validate === 'function') {
|
|
// Returns "tuples" of [id, error string]
|
|
errors = validate({model: this, id, form: this.getData()}) || [];
|
|
}
|
|
|
|
const fieldIsRequiredMessage = t('Field is required');
|
|
|
|
if (!this.isValidRequiredField(id)) {
|
|
errors.push([id, fieldIsRequiredMessage]);
|
|
}
|
|
|
|
// If we have no errors, ensure we clear the field
|
|
errors = errors.length === 0 ? [[id, null]] : errors;
|
|
|
|
errors.forEach(([field, errorMessage]) => this.setError(field, errorMessage));
|
|
return undefined;
|
|
}
|
|
|
|
updateShowSaveState(id: string, value: FieldValue) {
|
|
const isValueChanged = value !== this.initialData[id];
|
|
// Update field state to "show save" if save on blur is disabled for this field
|
|
// (only if contents of field differs from initial value)
|
|
const saveOnBlurFieldOverride = this.getDescriptor(id, 'saveOnBlur');
|
|
if (typeof saveOnBlurFieldOverride === 'undefined' || saveOnBlurFieldOverride) {
|
|
return;
|
|
}
|
|
if (this.getFieldState(id, 'showSave') === isValueChanged) {
|
|
return;
|
|
}
|
|
|
|
this.setFieldState(id, 'showSave', isValueChanged);
|
|
}
|
|
|
|
/**
|
|
* Changes form values to previous saved state
|
|
*/
|
|
undo() {
|
|
// Always have initial data snapshot
|
|
if (this.snapshots.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
this.snapshots.shift();
|
|
this.fields.replace(this.snapshots[0]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Attempts to save entire form to server and saves a snapshot for undos
|
|
*/
|
|
saveForm() {
|
|
if (!this.validateForm()) {
|
|
return null;
|
|
}
|
|
|
|
let saveSnapshot: SaveSnapshot = this.createSnapshot();
|
|
|
|
const request = this.doApiRequest({
|
|
data: this.getTransformedData(),
|
|
});
|
|
|
|
this.setFormSaving();
|
|
request
|
|
.then(resp => {
|
|
// save snapshot
|
|
if (saveSnapshot) {
|
|
saveSnapshot();
|
|
saveSnapshot = null;
|
|
}
|
|
|
|
if (this.options.onSubmitSuccess) {
|
|
this.options.onSubmitSuccess(resp, this);
|
|
}
|
|
})
|
|
.catch(resp => {
|
|
// should we revert field value to last known state?
|
|
saveSnapshot = null;
|
|
if (this.options.resetOnError) {
|
|
this.setInitialData({});
|
|
}
|
|
this.submitError(resp);
|
|
if (this.options.onSubmitError) {
|
|
this.options.onSubmitError(resp, this);
|
|
}
|
|
});
|
|
|
|
return request;
|
|
}
|
|
|
|
/**
|
|
* Attempts to save field and show undo message if necessary.
|
|
* Calls submit handlers.
|
|
* TODO(billy): This should return a promise that resolves (instead of null)
|
|
*/
|
|
saveField(id: string, currentValue: FieldValue) {
|
|
const oldValue = this.initialData[id];
|
|
const savePromise = this.saveFieldRequest(id, currentValue);
|
|
|
|
if (!savePromise) {
|
|
return null;
|
|
}
|
|
|
|
return savePromise
|
|
.then(resp => {
|
|
const newValue = this.getValue(id);
|
|
const change = {old: oldValue, new: newValue};
|
|
|
|
// Only use `allowUndo` option if explicitly defined
|
|
if (typeof this.options.allowUndo === 'undefined' || this.options.allowUndo) {
|
|
saveOnBlurUndoMessage(change, this, id);
|
|
}
|
|
|
|
if (this.options.onSubmitSuccess) {
|
|
this.options.onSubmitSuccess(resp, this, id, change);
|
|
}
|
|
|
|
return resp;
|
|
})
|
|
.catch(error => {
|
|
if (this.options.onSubmitError) {
|
|
this.options.onSubmitError(error, this, id);
|
|
}
|
|
return {};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Saves a field with new value
|
|
*
|
|
* If field has changes, field does not have errors, then it will:
|
|
* Save a snapshot, apply any data transforms, perform api request.
|
|
*
|
|
* If successful then: 1) reset save state, 2) update `initialData`, 3) save snapshot
|
|
* If failed then: 1) reset save state, 2) add error state
|
|
*/
|
|
saveFieldRequest(id: string, currentValue: FieldValue) {
|
|
const initialValue = this.initialData[id];
|
|
|
|
// Don't save if field hasn't changed
|
|
// Don't need to check for error state since initialData wouldn't have updated since last error
|
|
if (
|
|
currentValue === initialValue ||
|
|
(currentValue === '' && !defined(initialValue))
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
// Check for error first
|
|
this.validateField(id);
|
|
if (!this.isValidField(id)) {
|
|
return null;
|
|
}
|
|
|
|
// shallow clone fields
|
|
let saveSnapshot: SaveSnapshot = this.createSnapshot();
|
|
|
|
// Save field + value
|
|
this.setSaving(id, true);
|
|
|
|
const getData = this.getDescriptor(id, 'getData');
|
|
|
|
// Check if field needs to handle transforming request object
|
|
const getDataFn = typeof getData === 'function' ? getData : a => a;
|
|
|
|
const request = this.doApiRequest({
|
|
data: getDataFn(
|
|
{[id]: this.getTransformedValue(id)},
|
|
{model: this, id, form: this.getData()}
|
|
),
|
|
});
|
|
|
|
request
|
|
.then(data => {
|
|
this.setSaving(id, false);
|
|
|
|
// save snapshot
|
|
if (saveSnapshot) {
|
|
saveSnapshot();
|
|
saveSnapshot = null;
|
|
}
|
|
|
|
// Update initialData after successfully saving a field as it will now be the baseline value
|
|
this.initialData[id] = this.getValue(id);
|
|
|
|
return data;
|
|
})
|
|
.catch(resp => {
|
|
// should we revert field value to last known state?
|
|
saveSnapshot = null;
|
|
|
|
// Field can be configured to reset on error
|
|
// e.g. BooleanFields
|
|
const shouldReset = this.getDescriptor(id, 'resetOnError');
|
|
if (shouldReset) {
|
|
this.setValue(id, initialValue);
|
|
}
|
|
|
|
// API can return a JSON object with either:
|
|
// 1) map of {[fieldName] => Array<ErrorMessages>}
|
|
// 2) {'non_field_errors' => Array<ErrorMessages>}
|
|
if (resp?.responseJSON) {
|
|
// non-field errors can be camelcase or snake case
|
|
const nonFieldErrors =
|
|
resp.responseJSON.non_field_errors || resp.responseJSON.nonFieldErrors;
|
|
|
|
// find the first entry with an error
|
|
const firstError = Object.entries(resp.responseJSON).find(
|
|
([_, v]) => Array.isArray(v) && v.length
|
|
)?.[1] as string | boolean | undefined;
|
|
|
|
// Show resp msg from API endpoint if possible
|
|
if (Array.isArray(resp.responseJSON[id]) && resp.responseJSON[id].length) {
|
|
// Just take first resp for now
|
|
this.setError(id, resp.responseJSON[id][0]);
|
|
} else if (Array.isArray(nonFieldErrors) && nonFieldErrors.length) {
|
|
addErrorMessage(nonFieldErrors[0], {duration: 10000});
|
|
// Reset saving state
|
|
this.setError(id, '');
|
|
} else if (firstError) {
|
|
this.setError(id, firstError);
|
|
} else {
|
|
this.setError(id, 'Failed to save');
|
|
}
|
|
} else {
|
|
// Default error behavior
|
|
this.setError(id, 'Failed to save');
|
|
}
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.error('Error saving form field', resp?.responseJSON);
|
|
});
|
|
|
|
return request;
|
|
}
|
|
|
|
/**
|
|
* This is called when a field is blurred
|
|
*
|
|
* If `saveOnBlur` is set then call `saveField` and handle form callbacks accordingly
|
|
*/
|
|
handleBlurField(id: string, currentValue: FieldValue) {
|
|
// Nothing to do if `saveOnBlur` is not on
|
|
if (!this.options.saveOnBlur) {
|
|
return null;
|
|
}
|
|
|
|
// Fields can individually set `saveOnBlur` to `false` (note this is ignored when `undefined`)
|
|
const saveOnBlurFieldOverride = this.getDescriptor(id, 'saveOnBlur');
|
|
if (typeof saveOnBlurFieldOverride !== 'undefined' && !saveOnBlurFieldOverride) {
|
|
return null;
|
|
}
|
|
|
|
return this.saveField(id, currentValue);
|
|
}
|
|
|
|
setFormSaving() {
|
|
this.formState = FormState.SAVING;
|
|
}
|
|
|
|
/**
|
|
* This is called when a field does not saveOnBlur and has an individual "Save" button
|
|
*/
|
|
handleSaveField(id: string, currentValue: FieldValue) {
|
|
const savePromise = this.saveField(id, currentValue);
|
|
|
|
if (!savePromise) {
|
|
return null;
|
|
}
|
|
|
|
return savePromise.then(() => {
|
|
this.setFieldState(id, 'showSave', false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Cancel "Save Field" state and revert form value back to initial value
|
|
*/
|
|
handleCancelSaveField(id: string) {
|
|
this.setValue(id, this.initialData[id]);
|
|
this.setFieldState(id, 'showSave', false);
|
|
}
|
|
|
|
setFieldState(id: string, key: string, value: FieldValue) {
|
|
const state = {
|
|
...(this.fieldState.get(id) || {}),
|
|
[key]: value,
|
|
};
|
|
this.fieldState.set(id, state);
|
|
}
|
|
|
|
/**
|
|
* Set "saving" state for field
|
|
*/
|
|
setSaving(id: string, value: FieldValue) {
|
|
// When saving, reset error state
|
|
this.setError(id, false);
|
|
this.setFieldState(id, FormState.SAVING, value);
|
|
this.setFieldState(id, FormState.READY, !value);
|
|
}
|
|
|
|
/**
|
|
* Set "error" state for field
|
|
*/
|
|
setError(id: string, error: boolean | string) {
|
|
// Note we don't keep error in `this.fieldState` so that we can easily
|
|
// See if the form is in an "error" state with the `isError` getter
|
|
if (error) {
|
|
this.formState = FormState.ERROR;
|
|
this.errors.set(id, error);
|
|
} else {
|
|
this.formState = FormState.READY;
|
|
this.errors.delete(id);
|
|
}
|
|
|
|
// Field should no longer to "saving", but is not necessarily "ready"
|
|
this.setFieldState(id, FormState.SAVING, false);
|
|
}
|
|
|
|
/**
|
|
* Returns true if there are no errors
|
|
*/
|
|
validateForm(): boolean {
|
|
Array.from(this.fieldDescriptor.keys()).forEach(id => !this.validateField(id));
|
|
|
|
return !this.isError;
|
|
}
|
|
|
|
handleErrorResponse({responseJSON: resp}: {responseJSON?: any} = {}) {
|
|
if (!resp) {
|
|
return;
|
|
}
|
|
|
|
// Show resp msg from API endpoint if possible
|
|
Object.keys(resp).forEach(id => {
|
|
// non-field errors can be camelcase or snake case
|
|
const nonFieldErrors = resp.non_field_errors || resp.nonFieldErrors;
|
|
if (
|
|
(id === 'non_field_errors' || id === 'nonFieldErrors') &&
|
|
Array.isArray(nonFieldErrors) &&
|
|
nonFieldErrors.length
|
|
) {
|
|
addErrorMessage(nonFieldErrors[0], {duration: 10000});
|
|
} else if (Array.isArray(resp[id]) && resp[id].length) {
|
|
// Just take first resp for now
|
|
this.setError(id, resp[id][0]);
|
|
}
|
|
});
|
|
}
|
|
|
|
submitSuccess(data: Record<string, FieldValue>) {
|
|
// update initial data
|
|
this.formState = FormState.READY;
|
|
this.initialData = data;
|
|
}
|
|
|
|
submitError(err: {responseJSON?: any}) {
|
|
this.formState = FormState.ERROR;
|
|
|
|
this.formErrors = this.options.mapFormErrors
|
|
? this.options.mapFormErrors(err.responseJSON)
|
|
: err.responseJSON;
|
|
|
|
this.handleErrorResponse({responseJSON: this.formErrors});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The mock model mocks the model interface to simply return values from the props
|
|
*
|
|
* This is valuable for using form fields outside of a Form context. Disables a
|
|
* lot of functionality however.
|
|
*/
|
|
export class MockModel {
|
|
// TODO(TS)
|
|
props: any;
|
|
|
|
initialData: Record<string, FieldValue>;
|
|
|
|
constructor(props) {
|
|
this.props = props;
|
|
|
|
this.initialData = {
|
|
[props.name]: props.value,
|
|
};
|
|
}
|
|
|
|
setValue() {}
|
|
|
|
setFieldDescriptor() {}
|
|
|
|
removeField() {}
|
|
|
|
handleBlurField() {}
|
|
|
|
getValue() {
|
|
return this.props.value;
|
|
}
|
|
|
|
getError() {
|
|
return this.props.error;
|
|
}
|
|
|
|
getFieldState() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export default FormModel;
|