newIssueExperienceButton.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import {useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import {motion} from 'framer-motion';
  4. import {Button} from 'sentry/components/button';
  5. import DropdownButton from 'sentry/components/dropdownButton';
  6. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  7. import {IconLab} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {defined} from 'sentry/utils';
  10. import {trackAnalytics} from 'sentry/utils/analytics';
  11. import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
  12. import useMutateUserOptions from 'sentry/utils/useMutateUserOptions';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import {useUser} from 'sentry/utils/useUser';
  15. import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
  16. export function NewIssueExperienceButton() {
  17. const user = useUser();
  18. const organization = useOrganization();
  19. const hasStreamlinedUIFlag = organization.features.includes('issue-details-streamline');
  20. const hasEnforceStreamlinedUIFlag = organization.features.includes(
  21. 'issue-details-streamline-enforce'
  22. );
  23. const hasOnlyOneUIOption = defined(organization.streamlineOnly);
  24. const hasStreamlinedUI = useHasStreamlinedUI();
  25. const openForm = useFeedbackForm();
  26. const {mutate} = useMutateUserOptions();
  27. const handleToggle = useCallback(() => {
  28. mutate({['prefersIssueDetailsStreamlinedUI']: !hasStreamlinedUI});
  29. trackAnalytics('issue_details.streamline_ui_toggle', {
  30. isEnabled: !hasStreamlinedUI,
  31. organization,
  32. });
  33. }, [mutate, organization, hasStreamlinedUI]);
  34. // We hide the toggle if the org...
  35. if (
  36. // doesn't have the 'opt-in' flag,
  37. !hasStreamlinedUIFlag ||
  38. // has the 'remove opt-out' flag,
  39. hasEnforceStreamlinedUIFlag ||
  40. // has access to only one interface (via the organization option).
  41. hasOnlyOneUIOption
  42. ) {
  43. return null;
  44. }
  45. if (!openForm || !hasStreamlinedUI) {
  46. const label = hasStreamlinedUI
  47. ? t('Switch to the old issue experience')
  48. : t('Switch to the new issue experience');
  49. const text = hasStreamlinedUI ? null : t('Try New UI');
  50. return (
  51. <ToggleButtonWrapper>
  52. <ToggleButton
  53. enabled={hasStreamlinedUI}
  54. size={hasStreamlinedUI ? 'xs' : 'sm'}
  55. icon={
  56. defined(user?.options?.prefersIssueDetailsStreamlinedUI) ? (
  57. <IconLab isSolid={hasStreamlinedUI} />
  58. ) : (
  59. <motion.div
  60. style={{height: 14}}
  61. animate={{
  62. rotate: [null, 6, -6, 12, -12, 6, -6, 0],
  63. }}
  64. transition={{
  65. duration: 1,
  66. delay: 1,
  67. repeatDelay: 3,
  68. repeat: 3,
  69. }}
  70. >
  71. <IconLab isSolid={hasStreamlinedUI} />
  72. </motion.div>
  73. )
  74. }
  75. title={label}
  76. aria-label={label}
  77. borderless={!hasStreamlinedUI}
  78. onClick={handleToggle}
  79. >
  80. {text ? <span>{text}</span> : null}
  81. <ToggleBorder />
  82. </ToggleButton>
  83. </ToggleButtonWrapper>
  84. );
  85. }
  86. return (
  87. <DropdownMenu
  88. trigger={triggerProps => (
  89. <StyledDropdownButton
  90. {...triggerProps}
  91. enabled={hasStreamlinedUI}
  92. size={hasStreamlinedUI ? 'xs' : 'sm'}
  93. aria-label={t('Switch issue experience')}
  94. >
  95. {/* Passing icon as child to avoid extra icon margin */}
  96. <IconLab isSolid={hasStreamlinedUI} />
  97. </StyledDropdownButton>
  98. )}
  99. items={[
  100. {
  101. key: 'switch-to-old-ui',
  102. label: t('Switch to the old issue experience'),
  103. onAction: handleToggle,
  104. },
  105. {
  106. key: 'give-feedback',
  107. label: t('Give feedback on new UI'),
  108. hidden: !openForm,
  109. onAction: () => {
  110. openForm({
  111. messagePlaceholder: t(
  112. 'Excluding bribes, what would make you excited to use the new UI?'
  113. ),
  114. tags: {
  115. ['feedback.source']: 'streamlined_issue_details',
  116. ['feedback.owner']: 'issues',
  117. },
  118. });
  119. },
  120. },
  121. ]}
  122. position="bottom-end"
  123. />
  124. );
  125. }
  126. const StyledDropdownButton = styled(DropdownButton)<{enabled: boolean}>`
  127. color: ${p => (p.enabled ? p.theme.button.primary.background : 'inherit')};
  128. :hover {
  129. color: ${p => (p.enabled ? p.theme.button.primary.background : 'inherit')};
  130. }
  131. `;
  132. const ToggleButtonWrapper = styled('div')`
  133. overflow: hidden;
  134. margin: 0 -1px;
  135. border-radius: 7px;
  136. `;
  137. const ToggleButton = styled(Button)<{enabled: boolean}>`
  138. position: relative;
  139. color: ${p => (p.enabled ? p.theme.button.primary.background : 'inherit')};
  140. :hover {
  141. color: ${p => (p.enabled ? p.theme.button.primary.background : 'inherit')};
  142. }
  143. &:after {
  144. position: absolute;
  145. content: '';
  146. inset: 0;
  147. background: ${p => p.theme.background};
  148. border-radius: ${p => p.theme.borderRadius};
  149. }
  150. span {
  151. z-index: 1;
  152. }
  153. `;
  154. const ToggleBorder = styled('div')`
  155. @keyframes rotating {
  156. from {
  157. transform: rotate(0deg);
  158. }
  159. to {
  160. transform: rotate(360deg);
  161. }
  162. }
  163. position: absolute;
  164. content: '';
  165. z-index: -1;
  166. width: 125px;
  167. height: 125px;
  168. border-radius: 7px;
  169. background: ${p => p.theme.badge.beta.background};
  170. animation: rotating 10s linear infinite;
  171. `;