model.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. import isEqual from 'lodash/isEqual';
  2. import {action, computed, makeObservable, observable, ObservableMap} from 'mobx';
  3. import {addErrorMessage, saveOnBlurUndoMessage} from 'sentry/actionCreators/indicator';
  4. import {APIRequestMethod, Client} from 'sentry/api';
  5. import FormState from 'sentry/components/forms/state';
  6. import {t} from 'sentry/locale';
  7. import type {Choice} from 'sentry/types';
  8. import {defined} from 'sentry/utils';
  9. type Snapshot = Map<string, FieldValue>;
  10. type SaveSnapshot = (() => number) | null;
  11. export type FieldValue = string | number | boolean | Choice | undefined; // is undefined valid here?
  12. export type FormOptions = {
  13. /**
  14. * Does the form support undo?
  15. */
  16. allowUndo?: boolean;
  17. /**
  18. * API endpoint use when saving the form model
  19. */
  20. apiEndpoint?: string;
  21. /**
  22. * API method used to save the form model
  23. */
  24. apiMethod?: APIRequestMethod;
  25. /**
  26. * Options passed to the API Client
  27. */
  28. apiOptions?: ConstructorParameters<typeof Client>[0];
  29. /**
  30. * Initial form data
  31. */
  32. initialData?: Record<string, FieldValue>;
  33. /**
  34. * Callback triggered when a field changes value
  35. */
  36. onFieldChange?: (id: string, finalValue: FieldValue) => void;
  37. /**
  38. * Callback triggered when form submission fails
  39. */
  40. onSubmitError?: (error: any, instance: FormModel, id?: string) => void;
  41. /**
  42. * Callback triggered when the form is successfully submitted
  43. */
  44. onSubmitSuccess?: (
  45. response: any,
  46. instance: FormModel,
  47. id?: string,
  48. change?: {new: FieldValue; old: FieldValue}
  49. ) => void;
  50. /**
  51. * Should the form reset when an error occurs?
  52. */
  53. resetOnError?: boolean;
  54. /**
  55. * Should the form save on blur?
  56. */
  57. saveOnBlur?: boolean;
  58. /**
  59. * Custom transformer function used before the API request
  60. */
  61. transformData?: (data: Record<string, any>, instance: FormModel) => Record<string, any>;
  62. };
  63. class FormModel {
  64. /**
  65. * Map of field name -> value
  66. */
  67. fields: ObservableMap<string, FieldValue> = observable.map();
  68. /**
  69. * Errors for individual fields
  70. * Note we don't keep error in `this.fieldState` so that we can easily
  71. * See if the form is in an "error" state with the `isError` getter
  72. */
  73. errors = new Map();
  74. /**
  75. * State of individual fields
  76. *
  77. * Map of field name -> object
  78. */
  79. fieldState = new Map();
  80. /**
  81. * State of the form as a whole
  82. */
  83. formState: FormState | undefined;
  84. /**
  85. * Holds field properties as declared in <Form>
  86. * Does not need to be observable since these props should never change
  87. */
  88. fieldDescriptor = new Map();
  89. /**
  90. * Holds a list of `fields` states
  91. */
  92. snapshots: Array<Snapshot> = [];
  93. /**
  94. * POJO of field name -> value
  95. * It holds field values "since last save"
  96. */
  97. initialData: Record<string, FieldValue> = {};
  98. api: Client;
  99. // TODO(epurkhiser): it looks like this can goa away, along with mapFormErrors,
  100. // unclear why this is part of the form model
  101. formErrors: any;
  102. options: FormOptions;
  103. constructor({initialData, apiOptions, ...options}: FormOptions = {}) {
  104. makeObservable(this, {
  105. fields: observable,
  106. errors: observable,
  107. fieldState: observable,
  108. formState: observable,
  109. isError: computed,
  110. isSaving: computed,
  111. formData: computed,
  112. formChanged: computed,
  113. resetForm: action,
  114. setFieldDescriptor: action,
  115. removeField: action,
  116. setValue: action,
  117. validateField: action,
  118. updateShowSaveState: action,
  119. undo: action,
  120. saveForm: action,
  121. saveField: action,
  122. saveFieldRequest: action,
  123. handleBlurField: action,
  124. setFormSaving: action,
  125. handleSaveField: action,
  126. handleCancelSaveField: action,
  127. setFieldState: action,
  128. setSaving: action,
  129. setError: action,
  130. validateForm: action,
  131. handleErrorResponse: action,
  132. submitSuccess: action,
  133. submitError: action,
  134. });
  135. this.options = options ?? {};
  136. if (initialData) {
  137. this.setInitialData(initialData);
  138. }
  139. this.api = new Client(apiOptions);
  140. }
  141. /**
  142. * Reset state of model
  143. */
  144. reset() {
  145. this.api.clear();
  146. this.fieldDescriptor.clear();
  147. this.resetForm();
  148. }
  149. resetForm() {
  150. this.fields.clear();
  151. this.errors.clear();
  152. this.fieldState.clear();
  153. this.snapshots = [];
  154. this.initialData = {};
  155. }
  156. /**
  157. * Deep equality comparison between last saved state and current fields state
  158. */
  159. get formChanged() {
  160. return !isEqual(this.initialData, Object.fromEntries(this.fields.toJSON()));
  161. }
  162. get formData() {
  163. return this.fields;
  164. }
  165. /**
  166. * Is form saving
  167. */
  168. get isSaving() {
  169. return this.formState === FormState.SAVING;
  170. }
  171. /**
  172. * Does form have any errors
  173. */
  174. get isError() {
  175. return !!this.errors.size;
  176. }
  177. /**
  178. * Sets initial form data
  179. *
  180. * Also resets snapshots
  181. */
  182. setInitialData(initialData?: Record<string, FieldValue>) {
  183. this.fields.replace(initialData || {});
  184. this.initialData = Object.fromEntries(this.fields.toJSON()) || {};
  185. this.snapshots = [new Map(this.fields.entries())];
  186. }
  187. /**
  188. * Set form options
  189. */
  190. setFormOptions(options: FormOptions) {
  191. this.options = {...this.options, ...options} || {};
  192. }
  193. /**
  194. * Set field properties
  195. */
  196. setFieldDescriptor(id: string, props) {
  197. // TODO(TS): add type to props
  198. this.fieldDescriptor.set(id, props);
  199. // Set default value iff initialData for field is undefined
  200. // This must take place before checking for `props.setValue` so that it can
  201. // be applied to `defaultValue`
  202. if (
  203. typeof props.defaultValue !== 'undefined' &&
  204. typeof this.initialData[id] === 'undefined'
  205. ) {
  206. this.initialData[id] =
  207. typeof props.defaultValue === 'function'
  208. ? props.defaultValue()
  209. : props.defaultValue;
  210. this.fields.set(id, this.initialData[id]);
  211. }
  212. if (typeof props.setValue === 'function') {
  213. this.initialData[id] = props.setValue(this.initialData[id], props);
  214. this.fields.set(id, this.initialData[id]);
  215. }
  216. }
  217. /**
  218. * Remove a field from the descriptor map and errors.
  219. */
  220. removeField(id: string) {
  221. this.fieldDescriptor.delete(id);
  222. this.errors.delete(id);
  223. }
  224. /**
  225. * Creates a cloned Map of `this.fields` and returns a closure that when called
  226. * will save Map to `snapshots
  227. */
  228. createSnapshot() {
  229. const snapshot = new Map(this.fields.entries());
  230. return () => this.snapshots.unshift(snapshot);
  231. }
  232. getDescriptor(id: string, key: string) {
  233. // Needs to call `has` or else component will not be reactive if `id` doesn't exist in observable map
  234. const descriptor = this.fieldDescriptor.has(id) && this.fieldDescriptor.get(id);
  235. if (!descriptor) {
  236. return null;
  237. }
  238. return descriptor[key];
  239. }
  240. getFieldState(id: string, key: string) {
  241. // Needs to call `has` or else component will not be reactive if `id` doesn't exist in observable map
  242. const fieldState = this.fieldState.has(id) && this.fieldState.get(id);
  243. if (!fieldState) {
  244. return null;
  245. }
  246. return fieldState[key];
  247. }
  248. getValue(id: string) {
  249. return this.fields.has(id) ? this.fields.get(id) : '';
  250. }
  251. getTransformedValue(id: string) {
  252. const getValue = this.getDescriptor(id, 'getValue');
  253. const transformer = typeof getValue === 'function' ? getValue : null;
  254. const value = this.getValue(id);
  255. return transformer ? transformer(value) : value;
  256. }
  257. /**
  258. * Data represented in UI
  259. */
  260. getData() {
  261. return Object.fromEntries(this.fields.toJSON());
  262. }
  263. /**
  264. * Form data that will be sent to API endpoint (i.e. after transforms)
  265. */
  266. getTransformedData() {
  267. const form = this.getData();
  268. const data = Object.keys(form)
  269. .map(id => [id, this.getTransformedValue(id)])
  270. .reduce<Record<string, any>>((acc, [id, value]) => {
  271. acc[id] = value;
  272. return acc;
  273. }, {});
  274. return this.options.transformData ? this.options.transformData(data, this) : data;
  275. }
  276. getError(id: string) {
  277. return this.errors.has(id) && this.errors.get(id);
  278. }
  279. /**
  280. * Returns true if not required or is required and is not empty
  281. */
  282. isValidRequiredField(id: string) {
  283. // Check field descriptor to see if field is required
  284. const isRequired = this.getDescriptor(id, 'required');
  285. const value = this.getValue(id);
  286. return !isRequired || (value !== '' && defined(value));
  287. }
  288. isValidField(id: string) {
  289. return (this.getError(id) || []).length === 0;
  290. }
  291. doApiRequest({
  292. apiEndpoint,
  293. apiMethod,
  294. data,
  295. }: {
  296. data: object;
  297. apiEndpoint?: string;
  298. apiMethod?: APIRequestMethod;
  299. }) {
  300. const endpoint = apiEndpoint || this.options.apiEndpoint || '';
  301. const method = apiMethod || this.options.apiMethod;
  302. return new Promise((resolve, reject) =>
  303. this.api.request(endpoint, {
  304. method,
  305. data,
  306. success: response => resolve(response),
  307. error: error => reject(error),
  308. })
  309. );
  310. }
  311. /**
  312. * Set the value of the form field
  313. * if quiet is true, we skip callbacks, validations
  314. */
  315. setValue(id: string, value: FieldValue, {quiet}: {quiet?: boolean} = {}) {
  316. const transformInput = this.getDescriptor(id, 'transformInput');
  317. const finalValue =
  318. typeof transformInput === 'function' ? transformInput(value) : value;
  319. this.fields.set(id, finalValue);
  320. if (quiet) {
  321. return;
  322. }
  323. if (this.options.onFieldChange) {
  324. this.options.onFieldChange(id, finalValue);
  325. }
  326. this.validateField(id);
  327. this.updateShowSaveState(id, finalValue);
  328. }
  329. validateField(id: string) {
  330. const validate = this.getDescriptor(id, 'validate');
  331. let errors: any[] = [];
  332. if (typeof validate === 'function') {
  333. // Returns "tuples" of [id, error string]
  334. errors = validate({model: this, id, form: this.getData()}) || [];
  335. }
  336. const fieldIsRequiredMessage = t('Field is required');
  337. if (!this.isValidRequiredField(id)) {
  338. errors.push([id, fieldIsRequiredMessage]);
  339. }
  340. // If we have no errors, ensure we clear the field
  341. errors = errors.length === 0 ? [[id, null]] : errors;
  342. errors.forEach(([field, errorMessage]) => this.setError(field, errorMessage));
  343. return undefined;
  344. }
  345. updateShowSaveState(id: string, value: FieldValue) {
  346. const isValueChanged = value !== this.initialData[id];
  347. // Update field state to "show save" if save on blur is disabled for this field
  348. // (only if contents of field differs from initial value)
  349. const saveOnBlurFieldOverride = this.getDescriptor(id, 'saveOnBlur');
  350. if (typeof saveOnBlurFieldOverride === 'undefined' || saveOnBlurFieldOverride) {
  351. return;
  352. }
  353. if (this.getFieldState(id, 'showSave') === isValueChanged) {
  354. return;
  355. }
  356. this.setFieldState(id, 'showSave', isValueChanged);
  357. }
  358. /**
  359. * Changes form values to previous saved state
  360. */
  361. undo() {
  362. // Always have initial data snapshot
  363. if (this.snapshots.length < 2) {
  364. return null;
  365. }
  366. this.snapshots.shift();
  367. this.fields.replace(this.snapshots[0]);
  368. return true;
  369. }
  370. /**
  371. * Attempts to save entire form to server and saves a snapshot for undos
  372. */
  373. saveForm() {
  374. if (!this.validateForm()) {
  375. return null;
  376. }
  377. let saveSnapshot: SaveSnapshot = this.createSnapshot();
  378. const request = this.doApiRequest({
  379. data: this.getTransformedData(),
  380. });
  381. this.setFormSaving();
  382. request
  383. .then(resp => {
  384. // save snapshot
  385. if (saveSnapshot) {
  386. saveSnapshot();
  387. saveSnapshot = null;
  388. }
  389. if (this.options.onSubmitSuccess) {
  390. this.options.onSubmitSuccess(resp, this);
  391. }
  392. })
  393. .catch(resp => {
  394. // should we revert field value to last known state?
  395. saveSnapshot = null;
  396. if (this.options.resetOnError) {
  397. this.setInitialData({});
  398. }
  399. this.submitError(resp);
  400. if (this.options.onSubmitError) {
  401. this.options.onSubmitError(resp, this);
  402. }
  403. });
  404. return request;
  405. }
  406. /**
  407. * Attempts to save field and show undo message if necessary.
  408. * Calls submit handlers.
  409. * TODO(billy): This should return a promise that resolves (instead of null)
  410. */
  411. saveField(id: string, currentValue: FieldValue) {
  412. const oldValue = this.initialData[id];
  413. const savePromise = this.saveFieldRequest(id, currentValue);
  414. if (!savePromise) {
  415. return null;
  416. }
  417. return savePromise
  418. .then(resp => {
  419. const newValue = this.getValue(id);
  420. const change = {old: oldValue, new: newValue};
  421. // Only use `allowUndo` option if explicitly defined
  422. if (typeof this.options.allowUndo === 'undefined' || this.options.allowUndo) {
  423. saveOnBlurUndoMessage(change, this, id);
  424. }
  425. if (this.options.onSubmitSuccess) {
  426. this.options.onSubmitSuccess(resp, this, id, change);
  427. }
  428. return resp;
  429. })
  430. .catch(error => {
  431. if (this.options.onSubmitError) {
  432. this.options.onSubmitError(error, this, id);
  433. }
  434. return {};
  435. });
  436. }
  437. /**
  438. * Saves a field with new value
  439. *
  440. * If field has changes, field does not have errors, then it will:
  441. * Save a snapshot, apply any data transforms, perform api request.
  442. *
  443. * If successful then: 1) reset save state, 2) update `initialData`, 3) save snapshot
  444. * If failed then: 1) reset save state, 2) add error state
  445. */
  446. saveFieldRequest(id: string, currentValue: FieldValue) {
  447. const initialValue = this.initialData[id];
  448. // Don't save if field hasn't changed
  449. // Don't need to check for error state since initialData wouldn't have updated since last error
  450. if (
  451. currentValue === initialValue ||
  452. (currentValue === '' && !defined(initialValue))
  453. ) {
  454. return null;
  455. }
  456. // Check for error first
  457. this.validateField(id);
  458. if (!this.isValidField(id)) {
  459. return null;
  460. }
  461. // shallow clone fields
  462. let saveSnapshot: SaveSnapshot = this.createSnapshot();
  463. // Save field + value
  464. this.setSaving(id, true);
  465. const getData = this.getDescriptor(id, 'getData');
  466. // Check if field needs to handle transforming request object
  467. const getDataFn = typeof getData === 'function' ? getData : a => a;
  468. const request = this.doApiRequest({
  469. data: getDataFn(
  470. {[id]: this.getTransformedValue(id)},
  471. {model: this, id, form: this.getData()}
  472. ),
  473. });
  474. request
  475. .then(data => {
  476. this.setSaving(id, false);
  477. // save snapshot
  478. if (saveSnapshot) {
  479. saveSnapshot();
  480. saveSnapshot = null;
  481. }
  482. // Update initialData after successfully saving a field as it will now be the baseline value
  483. this.initialData[id] = this.getValue(id);
  484. return data;
  485. })
  486. .catch(resp => {
  487. // should we revert field value to last known state?
  488. saveSnapshot = null;
  489. // Field can be configured to reset on error
  490. // e.g. BooleanFields
  491. const shouldReset = this.getDescriptor(id, 'resetOnError');
  492. if (shouldReset) {
  493. this.setValue(id, initialValue);
  494. }
  495. // API can return a JSON object with either:
  496. // 1) map of {[fieldName] => Array<ErrorMessages>}
  497. // 2) {'non_field_errors' => Array<ErrorMessages>}
  498. if (resp && resp.responseJSON) {
  499. // non-field errors can be camelcase or snake case
  500. const nonFieldErrors =
  501. resp.responseJSON.non_field_errors || resp.responseJSON.nonFieldErrors;
  502. // find the first entry with an error
  503. const firstError = Object.entries(resp.responseJSON).find(
  504. ([_, v]) => Array.isArray(v) && v.length
  505. )?.[1] as string | boolean | undefined;
  506. // Show resp msg from API endpoint if possible
  507. if (Array.isArray(resp.responseJSON[id]) && resp.responseJSON[id].length) {
  508. // Just take first resp for now
  509. this.setError(id, resp.responseJSON[id][0]);
  510. } else if (Array.isArray(nonFieldErrors) && nonFieldErrors.length) {
  511. addErrorMessage(nonFieldErrors[0], {duration: 10000});
  512. // Reset saving state
  513. this.setError(id, '');
  514. } else if (firstError) {
  515. this.setError(id, firstError);
  516. } else {
  517. this.setError(id, 'Failed to save');
  518. }
  519. } else {
  520. // Default error behavior
  521. this.setError(id, 'Failed to save');
  522. }
  523. // eslint-disable-next-line no-console
  524. console.error('Error saving form field', resp && resp.responseJSON);
  525. });
  526. return request;
  527. }
  528. /**
  529. * This is called when a field is blurred
  530. *
  531. * If `saveOnBlur` is set then call `saveField` and handle form callbacks accordingly
  532. */
  533. handleBlurField(id: string, currentValue: FieldValue) {
  534. // Nothing to do if `saveOnBlur` is not on
  535. if (!this.options.saveOnBlur) {
  536. return null;
  537. }
  538. // Fields can individually set `saveOnBlur` to `false` (note this is ignored when `undefined`)
  539. const saveOnBlurFieldOverride = this.getDescriptor(id, 'saveOnBlur');
  540. if (typeof saveOnBlurFieldOverride !== 'undefined' && !saveOnBlurFieldOverride) {
  541. return null;
  542. }
  543. return this.saveField(id, currentValue);
  544. }
  545. setFormSaving() {
  546. this.formState = FormState.SAVING;
  547. }
  548. /**
  549. * This is called when a field does not saveOnBlur and has an individual "Save" button
  550. */
  551. handleSaveField(id: string, currentValue: FieldValue) {
  552. const savePromise = this.saveField(id, currentValue);
  553. if (!savePromise) {
  554. return null;
  555. }
  556. return savePromise.then(() => {
  557. this.setFieldState(id, 'showSave', false);
  558. });
  559. }
  560. /**
  561. * Cancel "Save Field" state and revert form value back to initial value
  562. */
  563. handleCancelSaveField(id: string) {
  564. this.setValue(id, this.initialData[id]);
  565. this.setFieldState(id, 'showSave', false);
  566. }
  567. setFieldState(id: string, key: string, value: FieldValue) {
  568. const state = {
  569. ...(this.fieldState.get(id) || {}),
  570. [key]: value,
  571. };
  572. this.fieldState.set(id, state);
  573. }
  574. /**
  575. * Set "saving" state for field
  576. */
  577. setSaving(id: string, value: FieldValue) {
  578. // When saving, reset error state
  579. this.setError(id, false);
  580. this.setFieldState(id, FormState.SAVING, value);
  581. this.setFieldState(id, FormState.READY, !value);
  582. }
  583. /**
  584. * Set "error" state for field
  585. */
  586. setError(id: string, error: boolean | string) {
  587. // Note we don't keep error in `this.fieldState` so that we can easily
  588. // See if the form is in an "error" state with the `isError` getter
  589. if (error) {
  590. this.formState = FormState.ERROR;
  591. this.errors.set(id, error);
  592. } else {
  593. this.formState = FormState.READY;
  594. this.errors.delete(id);
  595. }
  596. // Field should no longer to "saving", but is not necessarily "ready"
  597. this.setFieldState(id, FormState.SAVING, false);
  598. }
  599. /**
  600. * Returns true if there are no errors
  601. */
  602. validateForm(): boolean {
  603. Array.from(this.fieldDescriptor.keys()).forEach(id => !this.validateField(id));
  604. return !this.isError;
  605. }
  606. handleErrorResponse({responseJSON: resp}: {responseJSON?: any} = {}) {
  607. if (!resp) {
  608. return;
  609. }
  610. // Show resp msg from API endpoint if possible
  611. Object.keys(resp).forEach(id => {
  612. // non-field errors can be camelcase or snake case
  613. const nonFieldErrors = resp.non_field_errors || resp.nonFieldErrors;
  614. if (
  615. (id === 'non_field_errors' || id === 'nonFieldErrors') &&
  616. Array.isArray(nonFieldErrors) &&
  617. nonFieldErrors.length
  618. ) {
  619. addErrorMessage(nonFieldErrors[0], {duration: 10000});
  620. } else if (Array.isArray(resp[id]) && resp[id].length) {
  621. // Just take first resp for now
  622. this.setError(id, resp[id][0]);
  623. }
  624. });
  625. }
  626. submitSuccess(data: Record<string, FieldValue>) {
  627. // update initial data
  628. this.formState = FormState.READY;
  629. this.initialData = data;
  630. }
  631. submitError(err: {responseJSON?: any}) {
  632. this.formState = FormState.ERROR;
  633. this.formErrors = this.mapFormErrors(err.responseJSON);
  634. this.handleErrorResponse({responseJSON: this.formErrors});
  635. }
  636. mapFormErrors(responseJSON?: any) {
  637. return responseJSON;
  638. }
  639. }
  640. /**
  641. * The mock model mocks the model interface to simply return values from the props
  642. *
  643. * This is valuable for using form fields outside of a Form context. Disables a
  644. * lot of functionality however.
  645. */
  646. export class MockModel {
  647. // TODO(TS)
  648. props: any;
  649. initialData: Record<string, FieldValue>;
  650. constructor(props) {
  651. this.props = props;
  652. this.initialData = {
  653. [props.name]: props.value,
  654. };
  655. }
  656. setValue() {}
  657. setFieldDescriptor() {}
  658. removeField() {}
  659. handleBlurField() {}
  660. getValue() {
  661. return this.props.value;
  662. }
  663. getError() {
  664. return this.props.error;
  665. }
  666. getFieldState() {
  667. return false;
  668. }
  669. }
  670. export default FormModel;