releaseActions.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import * as React from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import {archiveRelease, restoreRelease} from 'app/actionCreators/release';
  6. import {Client} from 'app/api';
  7. import Button from 'app/components/button';
  8. import ButtonBar from 'app/components/buttonBar';
  9. import Confirm from 'app/components/confirm';
  10. import DropdownLink from 'app/components/dropdownLink';
  11. import ProjectBadge from 'app/components/idBadge/projectBadge';
  12. import MenuItem from 'app/components/menuItem';
  13. import NavigationButtonGroup from 'app/components/navigationButtonGroup';
  14. import TextOverflow from 'app/components/textOverflow';
  15. import Tooltip from 'app/components/tooltip';
  16. import {IconEllipsis} from 'app/icons';
  17. import {t, tct, tn} from 'app/locale';
  18. import space from 'app/styles/space';
  19. import {Organization, Release, ReleaseMeta} from 'app/types';
  20. import {trackAnalyticsEvent} from 'app/utils/analytics';
  21. import {formatVersion} from 'app/utils/formatters';
  22. import {isReleaseArchived} from '../utils';
  23. type Props = {
  24. location: Location;
  25. organization: Organization;
  26. projectSlug: string;
  27. release: Release;
  28. releaseMeta: ReleaseMeta;
  29. refetchData: () => void;
  30. };
  31. function ReleaseActions({
  32. location,
  33. organization,
  34. projectSlug,
  35. release,
  36. releaseMeta,
  37. refetchData,
  38. }: Props) {
  39. async function handleArchive() {
  40. try {
  41. await archiveRelease(new Client(), {
  42. orgSlug: organization.slug,
  43. projectSlug,
  44. releaseVersion: release.version,
  45. });
  46. browserHistory.push(`/organizations/${organization.slug}/releases/`);
  47. } catch {
  48. // do nothing, action creator is already displaying error message
  49. }
  50. }
  51. async function handleRestore() {
  52. try {
  53. await restoreRelease(new Client(), {
  54. orgSlug: organization.slug,
  55. projectSlug,
  56. releaseVersion: release.version,
  57. });
  58. refetchData();
  59. } catch {
  60. // do nothing, action creator is already displaying error message
  61. }
  62. }
  63. function getProjectList() {
  64. const maxVisibleProjects = 5;
  65. const visibleProjects = releaseMeta.projects.slice(0, maxVisibleProjects);
  66. const numberOfCollapsedProjects =
  67. releaseMeta.projects.length - visibleProjects.length;
  68. return (
  69. <React.Fragment>
  70. {visibleProjects.map(project => (
  71. <ProjectBadge key={project.slug} project={project} avatarSize={18} />
  72. ))}
  73. {numberOfCollapsedProjects > 0 && (
  74. <span>
  75. <Tooltip
  76. title={release.projects
  77. .slice(maxVisibleProjects)
  78. .map(p => p.slug)
  79. .join(', ')}
  80. >
  81. + {tn('%s other project', '%s other projects', numberOfCollapsedProjects)}
  82. </Tooltip>
  83. </span>
  84. )}
  85. </React.Fragment>
  86. );
  87. }
  88. function getModalHeader(title: React.ReactNode) {
  89. return (
  90. <h4>
  91. <TextOverflow>{title}</TextOverflow>
  92. </h4>
  93. );
  94. }
  95. function getModalMessage(message: React.ReactNode) {
  96. return (
  97. <React.Fragment>
  98. {message}
  99. <ProjectsWrapper>{getProjectList()}</ProjectsWrapper>
  100. {t('Are you sure you want to do this?')}
  101. </React.Fragment>
  102. );
  103. }
  104. function replaceReleaseUrl(toRelease: string | null) {
  105. return toRelease
  106. ? {
  107. pathname: location.pathname
  108. .replace(encodeURIComponent(release.version), toRelease)
  109. .replace(release.version, toRelease),
  110. query: {...location.query, activeRepo: undefined},
  111. }
  112. : '';
  113. }
  114. function handleNavigationClick(direction: string) {
  115. trackAnalyticsEvent({
  116. eventKey: `release_detail.pagination`,
  117. eventName: `Release Detail: Pagination`,
  118. organization_id: parseInt(organization.id, 10),
  119. direction,
  120. });
  121. }
  122. const {
  123. nextReleaseVersion,
  124. prevReleaseVersion,
  125. firstReleaseVersion,
  126. lastReleaseVersion,
  127. } = release.currentProjectMeta;
  128. return (
  129. <ButtonBar gap={1}>
  130. <NavigationButtonGroup
  131. hasPrevious={!!prevReleaseVersion}
  132. hasNext={!!nextReleaseVersion}
  133. links={[
  134. replaceReleaseUrl(firstReleaseVersion),
  135. replaceReleaseUrl(prevReleaseVersion),
  136. replaceReleaseUrl(nextReleaseVersion),
  137. replaceReleaseUrl(lastReleaseVersion),
  138. ]}
  139. onOldestClick={() => handleNavigationClick('oldest')}
  140. onOlderClick={() => handleNavigationClick('older')}
  141. onNewerClick={() => handleNavigationClick('newer')}
  142. onNewestClick={() => handleNavigationClick('newest')}
  143. />
  144. <StyledDropdownLink
  145. caret={false}
  146. anchorRight={window.innerWidth > 992}
  147. title={<ActionsButton icon={<IconEllipsis />} label={t('Actions')} />}
  148. >
  149. {isReleaseArchived(release) ? (
  150. <Confirm
  151. onConfirm={handleRestore}
  152. header={getModalHeader(
  153. tct('Restore Release [release]', {
  154. release: formatVersion(release.version),
  155. })
  156. )}
  157. message={getModalMessage(
  158. tn(
  159. 'You are restoring this release for the following project:',
  160. 'By restoring this release, you are also restoring it for the following projects:',
  161. releaseMeta.projects.length
  162. )
  163. )}
  164. cancelText={t('Nevermind')}
  165. confirmText={t('Restore')}
  166. >
  167. <MenuItem>{t('Restore')}</MenuItem>
  168. </Confirm>
  169. ) : (
  170. <Confirm
  171. onConfirm={handleArchive}
  172. header={getModalHeader(
  173. tct('Archive Release [release]', {
  174. release: formatVersion(release.version),
  175. })
  176. )}
  177. message={getModalMessage(
  178. tn(
  179. 'You are archiving this release for the following project:',
  180. 'By archiving this release, you are also archiving it for the following projects:',
  181. releaseMeta.projects.length
  182. )
  183. )}
  184. cancelText={t('Nevermind')}
  185. confirmText={t('Archive')}
  186. >
  187. <MenuItem>{t('Archive')}</MenuItem>
  188. </Confirm>
  189. )}
  190. </StyledDropdownLink>
  191. </ButtonBar>
  192. );
  193. }
  194. const ActionsButton = styled(Button)`
  195. width: 40px;
  196. height: 40px;
  197. padding: 0;
  198. `;
  199. const StyledDropdownLink = styled(DropdownLink)`
  200. & + .dropdown-menu {
  201. top: 50px !important;
  202. }
  203. `;
  204. const ProjectsWrapper = styled('div')`
  205. margin: ${space(2)} 0 ${space(2)} ${space(2)};
  206. display: grid;
  207. gap: ${space(0.5)};
  208. img {
  209. border: none !important;
  210. box-shadow: none !important;
  211. }
  212. `;
  213. export default ReleaseActions;