shareIssue.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import {useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import ActionButton from 'sentry/components/actions/button';
  4. import AutoSelectText from 'sentry/components/autoSelectText';
  5. import Button from 'sentry/components/button';
  6. import Clipboard from 'sentry/components/clipboard';
  7. import Confirm from 'sentry/components/confirm';
  8. import DropdownLink from 'sentry/components/dropdownLink';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import Switch from 'sentry/components/switchButton';
  11. import {IconChevron, IconCopy, IconRefresh} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import space from 'sentry/styles/space';
  14. type ContainerProps = {
  15. onCancel: () => void;
  16. onConfirm: () => void;
  17. onConfirming: () => void;
  18. shareUrl: string;
  19. };
  20. type Props = {
  21. loading: boolean;
  22. /**
  23. * Called when refreshing an existing link
  24. */
  25. onReshare: () => void;
  26. onToggle: () => void;
  27. disabled?: boolean;
  28. /**
  29. * Link is public
  30. */
  31. isShared?: boolean;
  32. shareUrl?: string | null;
  33. };
  34. function ShareIssue({loading, onReshare, onToggle, disabled, isShared, shareUrl}: Props) {
  35. const [hasConfirmModal, setHasConfirmModal] = useState(false);
  36. // State of confirm modal so we can keep dropdown menu opn
  37. const handleConfirmCancel = () => {
  38. setHasConfirmModal(false);
  39. };
  40. const handleConfirmReshare = () => {
  41. setHasConfirmModal(true);
  42. };
  43. const handleToggleShare = (e: React.MouseEvent<HTMLButtonElement>) => {
  44. e.preventDefault();
  45. onToggle();
  46. };
  47. const handleOpen = () => {
  48. // Starts sharing as soon as dropdown is opened
  49. if (!loading && !isShared) {
  50. onToggle();
  51. }
  52. };
  53. return (
  54. <DropdownLink
  55. shouldIgnoreClickOutside={() => hasConfirmModal}
  56. customTitle={
  57. <ActionButton disabled={disabled}>
  58. <DropdownTitleContent>
  59. <IndicatorDot isShared={isShared} />
  60. {t('Share')}
  61. </DropdownTitleContent>
  62. <IconChevron direction="down" size="xs" />
  63. </ActionButton>
  64. }
  65. onOpen={handleOpen}
  66. disabled={disabled}
  67. keepMenuOpen
  68. >
  69. <DropdownContent>
  70. <Header>
  71. <Title>{t('Enable public share link')}</Title>
  72. <Switch isActive={isShared} size="sm" toggle={handleToggleShare} />
  73. </Header>
  74. {loading && (
  75. <LoadingContainer>
  76. <LoadingIndicator mini />
  77. </LoadingContainer>
  78. )}
  79. {!loading && isShared && shareUrl && (
  80. <ShareUrlContainer
  81. shareUrl={shareUrl}
  82. onCancel={handleConfirmCancel}
  83. onConfirming={handleConfirmReshare}
  84. onConfirm={onReshare}
  85. />
  86. )}
  87. </DropdownContent>
  88. </DropdownLink>
  89. );
  90. }
  91. export default ShareIssue;
  92. type UrlRef = React.ElementRef<typeof AutoSelectText>;
  93. function ShareUrlContainer({
  94. shareUrl,
  95. onConfirming,
  96. onCancel,
  97. onConfirm,
  98. }: ContainerProps) {
  99. const urlRef = useRef<UrlRef>(null);
  100. return (
  101. <UrlContainer>
  102. <TextContainer>
  103. <StyledAutoSelectText ref={urlRef}>{shareUrl}</StyledAutoSelectText>
  104. </TextContainer>
  105. <Clipboard hideUnsupported value={shareUrl}>
  106. <ClipboardButton
  107. title={t('Copy to clipboard')}
  108. borderless
  109. size="xs"
  110. onClick={() => urlRef.current?.selectText()}
  111. icon={<IconCopy />}
  112. aria-label={t('Copy to clipboard')}
  113. />
  114. </Clipboard>
  115. <Confirm
  116. message={t(
  117. 'You are about to regenerate a new shared URL. Your previously shared URL will no longer work. Do you want to continue?'
  118. )}
  119. onCancel={onCancel}
  120. onConfirming={onConfirming}
  121. onConfirm={onConfirm}
  122. >
  123. <ReshareButton
  124. title={t('Generate new URL')}
  125. aria-label={t('Generate new URL')}
  126. borderless
  127. size="xs"
  128. icon={<IconRefresh />}
  129. />
  130. </Confirm>
  131. </UrlContainer>
  132. );
  133. }
  134. const UrlContainer = styled('div')`
  135. display: flex;
  136. align-items: stretch;
  137. border: 1px solid ${p => p.theme.border};
  138. border-radius: ${space(0.5)};
  139. `;
  140. const LoadingContainer = styled('div')`
  141. display: flex;
  142. justify-content: center;
  143. `;
  144. const DropdownTitleContent = styled('div')`
  145. display: flex;
  146. align-items: center;
  147. margin-right: ${space(0.5)};
  148. `;
  149. const DropdownContent = styled('li')`
  150. padding: ${space(1.5)} ${space(2)};
  151. > div:not(:last-of-type) {
  152. margin-bottom: ${space(1.5)};
  153. }
  154. `;
  155. const Header = styled('div')`
  156. display: flex;
  157. justify-content: space-between;
  158. align-items: center;
  159. `;
  160. const Title = styled('div')`
  161. padding-right: ${space(4)};
  162. white-space: nowrap;
  163. font-size: ${p => p.theme.fontSizeMedium};
  164. font-weight: 600;
  165. `;
  166. const IndicatorDot = styled('span')<{isShared?: boolean}>`
  167. display: inline-block;
  168. margin-right: ${space(0.5)};
  169. border-radius: 50%;
  170. width: 10px;
  171. height: 10px;
  172. background: ${p => (p.isShared ? p.theme.active : p.theme.border)};
  173. `;
  174. const StyledAutoSelectText = styled(AutoSelectText)`
  175. flex: 1;
  176. padding: ${space(0.5)} 0 ${space(0.5)} ${space(0.75)};
  177. ${p => p.theme.overflowEllipsis}
  178. `;
  179. const TextContainer = styled('div')`
  180. position: relative;
  181. display: flex;
  182. flex: 1;
  183. background-color: transparent;
  184. border-right: 1px solid ${p => p.theme.border};
  185. max-width: 288px;
  186. `;
  187. const ClipboardButton = styled(Button)`
  188. border-radius: 0;
  189. border-right: 1px solid ${p => p.theme.border};
  190. height: 100%;
  191. &:hover {
  192. border-right: 1px solid ${p => p.theme.border};
  193. }
  194. `;
  195. const ReshareButton = styled(Button)`
  196. height: 100%;
  197. `;