selectField.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import {Component} from 'react';
  2. import {OptionsType, ValueType} from 'react-select';
  3. import {openConfirmModal} from 'sentry/components/confirm';
  4. import InputField, {InputFieldProps} from 'sentry/components/forms/inputField';
  5. import SelectControl, {ControlProps} from 'sentry/components/forms/selectControl';
  6. import {t} from 'sentry/locale';
  7. import {Choices, SelectValue} from 'sentry/types';
  8. export interface SelectFieldProps<OptionType>
  9. extends InputFieldProps,
  10. Omit<ControlProps<OptionType>, 'onChange'> {
  11. /**
  12. * Should the select be clearable?
  13. */
  14. allowClear?: boolean;
  15. /**
  16. * Should the select allow empty values?
  17. */
  18. allowEmpty?: boolean;
  19. /**
  20. * Allow specific options to be 'confirmed' with a confirmation message.
  21. *
  22. * The key is the value that should be confirmed, the value is the message
  23. * to display in the confirmation modal.
  24. *
  25. * XXX: This only works when using the new-style options format, and _only_
  26. * if the value object has a `value` attribute in the option. The types do
  27. * not correctly reflect this so be careful!
  28. */
  29. confirm?: Record<string, React.ReactNode>;
  30. /**
  31. * A label that is shown inside the select control.
  32. */
  33. inFieldLabel?: string;
  34. }
  35. function getChoices<T>(props: SelectFieldProps<T>): Choices {
  36. const choices = props.choices;
  37. if (typeof choices === 'function') {
  38. return choices(props);
  39. }
  40. if (choices === undefined) {
  41. return [];
  42. }
  43. return choices;
  44. }
  45. /**
  46. * Required to type guard for OptionsType<T> which is a readonly Array
  47. */
  48. function isArray<T>(maybe: T | OptionsType<T>): maybe is OptionsType<T> {
  49. return Array.isArray(maybe);
  50. }
  51. export default class SelectField<OptionType extends SelectValue<any>> extends Component<
  52. SelectFieldProps<OptionType>
  53. > {
  54. static defaultProps = {
  55. allowClear: false,
  56. allowEmpty: false,
  57. placeholder: '--',
  58. escapeMarkup: true,
  59. multiple: false,
  60. small: false,
  61. formatMessageValue: (value, props) =>
  62. (getChoices(props).find(choice => choice[0] === value) || [null, value])[1],
  63. };
  64. handleChange = (
  65. onBlur: InputFieldProps['onBlur'],
  66. onChange: InputFieldProps['onChange'],
  67. optionObj: ValueType<OptionType>
  68. ) => {
  69. let value: any = undefined;
  70. // If optionObj is empty, then it probably means that the field was "cleared"
  71. if (!optionObj) {
  72. value = optionObj;
  73. } else if (this.props.multiple && isArray(optionObj)) {
  74. // List of optionObjs
  75. value = optionObj.map(({value: val}) => val);
  76. } else if (!isArray(optionObj)) {
  77. value = optionObj.value;
  78. }
  79. onChange?.(value, {});
  80. onBlur?.(value, {});
  81. };
  82. render() {
  83. const {allowClear, confirm, multiple, ...otherProps} = this.props;
  84. return (
  85. <InputField
  86. {...otherProps}
  87. field={({onChange, onBlur, required: _required, ...props}) => (
  88. <SelectControl
  89. {...props}
  90. clearable={allowClear}
  91. multiple={multiple}
  92. onChange={val => {
  93. try {
  94. if (!confirm) {
  95. this.handleChange(onBlur, onChange, val);
  96. return;
  97. }
  98. // Support 'confirming' selections. This only works with
  99. // `val` objects that use the new-style options format
  100. const previousValue = props.value?.toString();
  101. // `val` may be null if clearing the select for an optional field
  102. const newValue = val?.value?.toString();
  103. // Value not marked for confirmation, or hasn't changed
  104. if (!confirm[newValue] || previousValue === newValue) {
  105. this.handleChange(onBlur, onChange, val);
  106. return;
  107. }
  108. openConfirmModal({
  109. onConfirm: () => this.handleChange(onBlur, onChange, val),
  110. message: confirm[val?.value] ?? t('Continue with these changes?'),
  111. });
  112. } catch (e) {
  113. // Swallow expected error to prevent bubbling up.
  114. if (e.message === 'Invalid selection. Field cannot be empty.') {
  115. return;
  116. }
  117. throw e;
  118. }
  119. }}
  120. />
  121. )}
  122. />
  123. );
  124. }
  125. }