draggableTabMenuButton.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import styled from '@emotion/styled';
  2. import {Button} from 'sentry/components/button';
  3. import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
  4. import {IconEllipsis, IconMegaphone} from 'sentry/icons';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
  8. interface DraggableTabMenuButtonProps {
  9. menuOptions: MenuItemProps[];
  10. 'aria-label'?: string;
  11. hasUnsavedChanges?: boolean;
  12. }
  13. export function DraggableTabMenuButton({
  14. hasUnsavedChanges = false,
  15. menuOptions,
  16. ...props
  17. }: DraggableTabMenuButtonProps) {
  18. return (
  19. <TriggerIconWrap>
  20. <StyledDropdownMenu
  21. position="bottom-start"
  22. triggerProps={{
  23. 'aria-label': props['aria-label'] ?? 'Tab Options',
  24. size: 'zero',
  25. showChevron: false,
  26. borderless: true,
  27. icon: (
  28. <ButtonWrapper>
  29. <IconEllipsis compact />
  30. {hasUnsavedChanges && (
  31. <UnsavedChangesIndicator
  32. role="presentation"
  33. data-test-id="unsaved-changes-indicator"
  34. />
  35. )}
  36. </ButtonWrapper>
  37. ),
  38. style: {width: '18px', height: '16px', borderRadius: '4px'},
  39. }}
  40. items={menuOptions}
  41. offset={[-10, 5]}
  42. menuFooter={<FeedbackFooter />}
  43. usePortal
  44. />
  45. </TriggerIconWrap>
  46. );
  47. }
  48. function FeedbackFooter() {
  49. const openForm = useFeedbackForm();
  50. if (!openForm) {
  51. return null;
  52. }
  53. return (
  54. <SectionedOverlayFooter>
  55. <Button
  56. size="xs"
  57. icon={<IconMegaphone />}
  58. onClick={() =>
  59. openForm({
  60. messagePlaceholder: t('How can we make custom views better for you?'),
  61. tags: {
  62. ['feedback.source']: 'custom_views',
  63. ['feedback.owner']: 'issues',
  64. },
  65. })
  66. }
  67. >
  68. {t('Give Feedback')}
  69. </Button>
  70. </SectionedOverlayFooter>
  71. );
  72. }
  73. const SectionedOverlayFooter = styled('div')`
  74. grid-area: footer;
  75. display: flex;
  76. align-items: center;
  77. justify-content: center;
  78. padding: ${space(1)};
  79. border-top: 1px solid ${p => p.theme.innerBorder};
  80. `;
  81. const StyledDropdownMenu = styled(DropdownMenu)`
  82. font-weight: ${p => p.theme.fontWeightNormal};
  83. `;
  84. const UnsavedChangesIndicator = styled('div')`
  85. width: 7px;
  86. height: 7px;
  87. border-radius: 50%;
  88. background: ${p => p.theme.active};
  89. border: solid 1px ${p => p.theme.background};
  90. position: absolute;
  91. top: -3px;
  92. right: -3px;
  93. `;
  94. const ButtonWrapper = styled('div')`
  95. width: 18px;
  96. height: 16px;
  97. border: 1px solid ${p => p.theme.gray200};
  98. border-radius: 4px;
  99. `;
  100. const TriggerIconWrap = styled('div')`
  101. position: relative;
  102. display: flex;
  103. align-items: center;
  104. `;