externalIssueActions.tsx 4.2 KB

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