selectCreatableField.tsx 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import styled from '@emotion/styled';
  2. import {StyledForm} from 'sentry/components/deprecatedforms/form';
  3. import SelectField from 'sentry/components/deprecatedforms/selectField';
  4. import SelectControl from 'sentry/components/forms/selectControl';
  5. import {SelectValue} from 'sentry/types';
  6. import {defined} from 'sentry/utils';
  7. import convertFromSelect2Choices from 'sentry/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 coerced 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, 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. value={this.state.value}
  62. onChange={this.onChange}
  63. clearable={clearable}
  64. multiple={this.isMultiple()}
  65. name={name}
  66. />
  67. );
  68. }
  69. }
  70. // This is because we are removing `control-group` class name which provides margin-bottom
  71. const StyledSelectControl = styled(SelectControl)`
  72. ${StyledForm} &, .form-stacked & {
  73. .control-group & {
  74. margin-bottom: 0;
  75. }
  76. margin-bottom: 15px;
  77. }
  78. `;