optionSelector.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import FeatureBadge from 'sentry/components/badge/featureBadge';
  4. import type {
  5. MultipleSelectProps,
  6. SelectOption,
  7. SingleSelectProps,
  8. } from 'sentry/components/compactSelect';
  9. import {CompactSelect} from 'sentry/components/compactSelect';
  10. import Truncate from 'sentry/components/truncate';
  11. import {defined} from 'sentry/utils';
  12. type BaseProps = {
  13. title: string;
  14. featureType?: 'alpha' | 'beta' | 'new';
  15. };
  16. interface SingleProps
  17. extends Omit<
  18. SingleSelectProps<string>,
  19. 'onChange' | 'defaultValue' | 'multiple' | 'title'
  20. >,
  21. BaseProps {
  22. onChange: (value: string) => void;
  23. selected: string;
  24. defaultValue?: string;
  25. multiple?: false;
  26. }
  27. interface MultipleProps
  28. extends Omit<
  29. MultipleSelectProps<string>,
  30. 'onChange' | 'defaultValue' | 'multiple' | 'title'
  31. >,
  32. BaseProps {
  33. multiple: true;
  34. onChange: (value: string[]) => void;
  35. selected: string[];
  36. defaultValue?: string[];
  37. }
  38. function OptionSelector({
  39. options,
  40. onChange,
  41. selected,
  42. title,
  43. featureType,
  44. multiple,
  45. defaultValue,
  46. closeOnSelect,
  47. ...rest
  48. }: SingleProps | MultipleProps) {
  49. const mappedOptions = useMemo(() => {
  50. return options.map(opt => ({
  51. ...opt,
  52. textValue: String(opt.label),
  53. label: <Truncate value={String(opt.label)} maxLength={60} expandDirection="left" />,
  54. }));
  55. }, [options]);
  56. const selectProps = useMemo(() => {
  57. // Use an if statement to help TS separate MultipleProps and SingleProps
  58. if (multiple) {
  59. return {
  60. multiple,
  61. value: selected,
  62. defaultValue,
  63. onChange: (sel: SelectOption<string>[]) => {
  64. onChange?.(sel.map(o => o.value));
  65. },
  66. closeOnSelect,
  67. };
  68. }
  69. return {
  70. multiple,
  71. value: selected,
  72. defaultValue,
  73. onChange: opt => onChange?.(opt.value),
  74. closeOnSelect,
  75. };
  76. }, [multiple, selected, defaultValue, onChange, closeOnSelect]);
  77. function isOptionDisabled(option) {
  78. return (
  79. // Option is explicitly marked as disabled
  80. // The user has reached the maximum number of selections (3), and the option hasn't
  81. // yet been selected. These options should be disabled to visually indicate that the
  82. // user has reached the max.
  83. option.disabled ||
  84. (multiple && selected.length === 3 && !selected.includes(option.value))
  85. );
  86. }
  87. return (
  88. <CompactSelect
  89. {...rest}
  90. {...selectProps}
  91. size="sm"
  92. options={mappedOptions}
  93. isOptionDisabled={isOptionDisabled}
  94. position="bottom-end"
  95. triggerProps={{
  96. borderless: true,
  97. prefix: (
  98. <Fragment>
  99. {title}
  100. {defined(featureType) ? <StyledFeatureBadge type={featureType} /> : null}
  101. </Fragment>
  102. ),
  103. }}
  104. />
  105. );
  106. }
  107. const StyledFeatureBadge = styled(FeatureBadge)`
  108. margin-left: 0px;
  109. `;
  110. export default OptionSelector;