selectCreatableField.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import styled from '@emotion/styled';
  2. import {StyledForm} from 'app/components/forms/form';
  3. import SelectControl from 'app/components/forms/selectControl';
  4. import SelectField from 'app/components/forms/selectField';
  5. import {SelectValue} from 'app/types';
  6. import {defined} from 'app/utils';
  7. import convertFromSelect2Choices from 'app/utils/convertFromSelect2Choices';
  8. /**
  9. * This is a <SelectField> that allows the user to create new options if one does't exist.
  10. *
  11. * This is used in some integrations
  12. */
  13. export default class SelectCreatableField extends SelectField {
  14. options: SelectValue<any>[] | undefined;
  15. constructor(props, context) {
  16. super(props, context);
  17. // We only want to parse options once because react-select relies
  18. // on `options` mutation when you create a new option
  19. //
  20. // Otherwise you will not get the created option in the dropdown menu
  21. this.options = this.getOptions(props);
  22. }
  23. UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
  24. const newError = this.getError(nextProps, nextContext);
  25. if (newError !== this.state.error) {
  26. this.setState({error: newError});
  27. }
  28. if (this.props.value !== nextProps.value || defined(nextContext.form)) {
  29. const newValue = this.getValue(nextProps, nextContext);
  30. // This is the only thing that is different from parent, we compare newValue against coerved value in state
  31. // To remain compatible with react-select, we need to store the option object that
  32. // includes `value` and `label`, but when we submit the format, we need to coerce it
  33. // to just return `value`. Also when field changes, it propagates the coerced value up
  34. const coercedValue = this.coerceValue(this.state.value);
  35. // newValue can be empty string because of `getValue`, while coerceValue needs to return null (to differentiate
  36. // empty string from cleared item). We could use `!=` to compare, but lets be a bit more explicit with strict equality
  37. //
  38. // This can happen when this is apart of a field, and it re-renders onChange for a different field,
  39. // there will be a mismatch between this component's state.value and `this.getValue` result above
  40. if (
  41. newValue !== coercedValue &&
  42. !!newValue !== !!coercedValue &&
  43. newValue !== this.state.value
  44. ) {
  45. this.setValue(newValue);
  46. }
  47. }
  48. }
  49. getOptions(props) {
  50. return convertFromSelect2Choices(props.choices) || props.options;
  51. }
  52. getField() {
  53. const {placeholder, disabled, required, clearable, name} = this.props;
  54. return (
  55. <StyledSelectControl
  56. creatable
  57. id={this.getId()}
  58. options={this.options}
  59. placeholder={placeholder}
  60. disabled={disabled}
  61. required={required}
  62. value={this.state.value}
  63. onChange={this.onChange}
  64. clearable={clearable}
  65. multiple={this.isMultiple()}
  66. name={name}
  67. />
  68. );
  69. }
  70. }
  71. // This is because we are removing `control-group` class name which provides margin-bottom
  72. const StyledSelectControl = styled(SelectControl)`
  73. ${StyledForm} &, .form-stacked & {
  74. .control-group & {
  75. margin-bottom: 0;
  76. }
  77. margin-bottom: 15px;
  78. }
  79. `;