externalIssueActions.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import {Client} from 'sentry/api';
  6. import AsyncComponent from 'sentry/components/asyncComponent';
  7. import IssueSyncListElement from 'sentry/components/issueSyncListElement';
  8. import {t} from 'sentry/locale';
  9. import space from 'sentry/styles/space';
  10. import {Group, GroupIntegration} from 'sentry/types';
  11. import withApi from 'sentry/utils/withApi';
  12. import IntegrationItem from 'sentry/views/organizationIntegrations/integrationItem';
  13. import ExternalIssueForm from './externalIssueForm';
  14. type Props = AsyncComponent['props'] & {
  15. api: Client;
  16. configurations: GroupIntegration[];
  17. group: Group;
  18. onChange: (onSuccess?: () => void, onError?: () => void) => void;
  19. };
  20. type LinkedIssues = {
  21. linked: GroupIntegration[];
  22. unlinked: GroupIntegration[];
  23. };
  24. const ExternalIssueActions = ({configurations, group, onChange, api}: Props) => {
  25. const {linked, unlinked} = configurations
  26. .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
  27. .reduce(
  28. (acc: LinkedIssues, curr) => {
  29. if (curr.externalIssues.length) {
  30. acc.linked.push(curr);
  31. } else {
  32. acc.unlinked.push(curr);
  33. }
  34. return acc;
  35. },
  36. {linked: [], unlinked: []}
  37. );
  38. const deleteIssue = (integration: GroupIntegration) => {
  39. const {externalIssues} = integration;
  40. // Currently we do not support a case where there is multiple external issues.
  41. // For example, we shouldn't have more than 1 jira ticket created for an issue for each jira configuration.
  42. const issue = externalIssues[0];
  43. const {id} = issue;
  44. const endpoint = `/groups/${group.id}/integrations/${integration.id}/?externalIssue=${id}`;
  45. api.request(endpoint, {
  46. method: 'DELETE',
  47. success: () => {
  48. onChange(
  49. () => addSuccessMessage(t('Successfully unlinked issue.')),
  50. () => addErrorMessage(t('Unable to unlink issue.'))
  51. );
  52. },
  53. error: () => {
  54. addErrorMessage(t('Unable to unlink issue.'));
  55. },
  56. });
  57. };
  58. const doOpenModal = (integration: GroupIntegration) =>
  59. openModal(
  60. deps => <ExternalIssueForm {...deps} {...{group, onChange, integration}} />,
  61. {allowClickClose: false}
  62. );
  63. return (
  64. <Fragment>
  65. {linked.map(config => {
  66. const {provider, externalIssues} = config;
  67. const issue = externalIssues[0];
  68. return (
  69. <IssueSyncListElement
  70. key={issue.id}
  71. externalIssueLink={issue.url}
  72. externalIssueId={issue.id}
  73. externalIssueKey={issue.key}
  74. externalIssueDisplayName={issue.displayName}
  75. onClose={() => deleteIssue(config)}
  76. integrationType={provider.key}
  77. hoverCardHeader={t('%s Integration', provider.name)}
  78. hoverCardBody={
  79. <div>
  80. <IssueTitle>{issue.title}</IssueTitle>
  81. {issue.description && (
  82. <IssueDescription>{issue.description}</IssueDescription>
  83. )}
  84. </div>
  85. }
  86. />
  87. );
  88. })}
  89. {unlinked.length > 0 && (
  90. <IssueSyncListElement
  91. integrationType={unlinked[0].provider.key}
  92. hoverCardHeader={t('%s Integration', unlinked[0].provider.name)}
  93. hoverCardBody={
  94. <Container>
  95. {unlinked.map(config => (
  96. <Wrapper onClick={() => doOpenModal(config)} key={config.id}>
  97. <IntegrationItem integration={config} />
  98. </Wrapper>
  99. ))}
  100. </Container>
  101. }
  102. onOpen={unlinked.length === 1 ? () => doOpenModal(unlinked[0]) : undefined}
  103. />
  104. )}
  105. </Fragment>
  106. );
  107. };
  108. const IssueTitle = styled('div')`
  109. font-size: 1.1em;
  110. font-weight: 600;
  111. ${p => p.theme.overflowEllipsis};
  112. `;
  113. const IssueDescription = styled('div')`
  114. margin-top: ${space(1)};
  115. ${p => p.theme.overflowEllipsis};
  116. `;
  117. const Wrapper = styled('div')`
  118. margin-bottom: ${space(2)};
  119. cursor: pointer;
  120. `;
  121. const Container = styled('div')`
  122. & > div:last-child {
  123. margin-bottom: ${space(1)};
  124. }
  125. `;
  126. export default withApi(ExternalIssueActions);