issueSyncListElement.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import * as React from 'react';
  2. import {ClassNames} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import capitalize from 'lodash/capitalize';
  5. import Hovercard from 'app/components/hovercard';
  6. import {IconAdd, IconClose} from 'app/icons';
  7. import space from 'app/styles/space';
  8. import {callIfFunction} from 'app/utils/callIfFunction';
  9. import {getIntegrationIcon} from 'app/utils/integrationUtil';
  10. type Props = {
  11. externalIssueLink?: string | null;
  12. externalIssueId?: string | null;
  13. externalIssueKey?: string | null;
  14. externalIssueDisplayName?: string | null;
  15. onOpen?: () => void;
  16. onClose?: (externalIssueId?: string | null) => void;
  17. integrationType?: string;
  18. hoverCardHeader?: React.ReactNode;
  19. hoverCardBody?: React.ReactNode;
  20. showHoverCard?: boolean;
  21. };
  22. class IssueSyncListElement extends React.Component<Props> {
  23. componentDidUpdate(nextProps) {
  24. if (
  25. this.props.showHoverCard !== nextProps.showHoverCard &&
  26. nextProps.showHoverCard === undefined
  27. ) {
  28. this.hoverCardRef.current && this.hoverCardRef.current.handleToggleOff();
  29. }
  30. }
  31. hoverCardRef = React.createRef<Hovercard>();
  32. isLinked(): boolean {
  33. return !!(this.props.externalIssueLink && this.props.externalIssueId);
  34. }
  35. handleDelete = (): void => {
  36. callIfFunction(this.props.onClose, this.props.externalIssueId);
  37. };
  38. handleIconClick = () => {
  39. if (this.isLinked()) {
  40. this.handleDelete();
  41. } else if (this.props.onOpen) {
  42. this.props.onOpen();
  43. }
  44. };
  45. getIcon(): React.ReactNode {
  46. return getIntegrationIcon(this.props.integrationType);
  47. }
  48. getPrettyName(): string {
  49. const type = this.props.integrationType;
  50. switch (type) {
  51. case 'gitlab':
  52. return 'GitLab';
  53. case 'github':
  54. return 'GitHub';
  55. case 'github_enterprise':
  56. return 'GitHub Enterprise';
  57. case 'vsts':
  58. return 'Azure DevOps';
  59. case 'jira_server':
  60. return 'Jira Server';
  61. default:
  62. return capitalize(type);
  63. }
  64. }
  65. getLink(): React.ReactElement {
  66. return (
  67. <IntegrationLink
  68. href={this.props.externalIssueLink || undefined}
  69. onClick={!this.isLinked() ? this.props.onOpen : undefined}
  70. >
  71. {this.getText()}
  72. </IntegrationLink>
  73. );
  74. }
  75. getText(): React.ReactNode {
  76. if (this.props.children) {
  77. return this.props.children;
  78. }
  79. if (this.props.externalIssueDisplayName) {
  80. return this.props.externalIssueDisplayName;
  81. }
  82. if (this.props.externalIssueKey) {
  83. return this.props.externalIssueKey;
  84. }
  85. return `Link ${this.getPrettyName()} Issue`;
  86. }
  87. render() {
  88. return (
  89. <IssueSyncListElementContainer>
  90. <ClassNames>
  91. {({css}) => (
  92. <Hovercard
  93. ref={this.hoverCardRef}
  94. containerClassName={css`
  95. display: flex;
  96. align-items: center;
  97. min-width: 0; /* flex-box overflow workaround */
  98. `}
  99. header={this.props.hoverCardHeader}
  100. body={this.props.hoverCardBody}
  101. bodyClassName="issue-list-body"
  102. show={this.props.showHoverCard}
  103. >
  104. {this.getIcon()}
  105. {this.getLink()}
  106. </Hovercard>
  107. )}
  108. </ClassNames>
  109. {(this.props.onClose || this.props.onOpen) && (
  110. <StyledIcon onClick={this.handleIconClick}>
  111. {this.isLinked() ? <IconClose /> : this.props.onOpen ? <IconAdd /> : null}
  112. </StyledIcon>
  113. )}
  114. </IssueSyncListElementContainer>
  115. );
  116. }
  117. }
  118. export const IssueSyncListElementContainer = styled('div')`
  119. line-height: 0;
  120. display: flex;
  121. align-items: center;
  122. justify-content: space-between;
  123. &:not(:last-child) {
  124. margin-bottom: ${space(2)};
  125. }
  126. `;
  127. export const IntegrationLink = styled('a')`
  128. text-decoration: none;
  129. padding-bottom: ${space(0.25)};
  130. margin-left: ${space(1)};
  131. color: ${p => p.theme.textColor};
  132. border-bottom: 1px solid ${p => p.theme.textColor};
  133. cursor: pointer;
  134. line-height: 1;
  135. white-space: nowrap;
  136. overflow: hidden;
  137. text-overflow: ellipsis;
  138. &,
  139. &:hover {
  140. border-bottom: 1px solid ${p => p.theme.blue300};
  141. }
  142. `;
  143. const StyledIcon = styled('span')`
  144. color: ${p => p.theme.textColor};
  145. cursor: pointer;
  146. `;
  147. export default IssueSyncListElement;