commitRow.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import {useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {openInviteMembersModal} from 'sentry/actionCreators/modal';
  5. import UserAvatar from 'sentry/components/avatar/userAvatar';
  6. import {Button} from 'sentry/components/button';
  7. import CommitLink from 'sentry/components/commitLink';
  8. import {Hovercard} from 'sentry/components/hovercard';
  9. import Link from 'sentry/components/links/link';
  10. import {PanelItem} from 'sentry/components/panels';
  11. import TextOverflow from 'sentry/components/textOverflow';
  12. import TimeSince from 'sentry/components/timeSince';
  13. import {IconWarning} from 'sentry/icons';
  14. import {t, tct} from 'sentry/locale';
  15. import ConfigStore from 'sentry/stores/configStore';
  16. import {space} from 'sentry/styles/space';
  17. import {Commit} from 'sentry/types';
  18. export function formatCommitMessage(message: string | null) {
  19. if (!message) {
  20. return t('No message provided');
  21. }
  22. return message.split(/\n/)[0];
  23. }
  24. export interface CommitRowProps {
  25. commit: Commit;
  26. customAvatar?: React.ReactNode;
  27. onCommitClick?: (commit: Commit) => void;
  28. onPullRequestClick?: () => void;
  29. }
  30. function CommitRow({
  31. commit,
  32. customAvatar,
  33. onPullRequestClick,
  34. onCommitClick,
  35. }: CommitRowProps) {
  36. const handleInviteClick = useCallback(() => {
  37. if (!commit.author?.email) {
  38. Sentry.captureException(
  39. new Error(`Commit author has no email or id, invite flow is broken.`)
  40. );
  41. return;
  42. }
  43. openInviteMembersModal({
  44. initialData: [
  45. {
  46. emails: new Set([commit.author.email]),
  47. },
  48. ],
  49. source: 'suspect_commit',
  50. });
  51. }, [commit.author]);
  52. const user = ConfigStore.get('user');
  53. const isUser = user?.id === commit.author?.id;
  54. return (
  55. <StyledPanelItem key={commit.id} data-test-id="commit-row">
  56. {customAvatar ? (
  57. customAvatar
  58. ) : commit.author && commit.author.id === undefined ? (
  59. <AvatarWrapper>
  60. <Hovercard
  61. skipWrapper
  62. body={
  63. <EmailWarning>
  64. {tct(
  65. 'The email [actorEmail] is not a member of your organization. [inviteUser:Invite] them or link additional emails in [accountSettings:account settings].',
  66. {
  67. actorEmail: <strong>{commit.author.email}</strong>,
  68. accountSettings: <StyledLink to="/settings/account/emails/" />,
  69. inviteUser: <StyledLink to="" onClick={handleInviteClick} />,
  70. }
  71. )}
  72. </EmailWarning>
  73. }
  74. >
  75. <UserAvatar size={36} user={commit.author} />
  76. <EmailWarningIcon data-test-id="email-warning">
  77. <IconWarning size="xs" />
  78. </EmailWarningIcon>
  79. </Hovercard>
  80. </AvatarWrapper>
  81. ) : (
  82. <div>
  83. <UserAvatar size={36} user={commit.author} />
  84. </div>
  85. )}
  86. <CommitMessage>
  87. <Message>{formatCommitMessage(commit.message)}</Message>
  88. <Meta>
  89. {tct('[author] committed [commitLink] \u2022 [date]', {
  90. author: (
  91. <strong>
  92. {isUser ? t('You') : commit.author?.name ?? t('Unknown author')}
  93. </strong>
  94. ),
  95. commitLink: (
  96. <CommitLink
  97. inline
  98. showIcon={false}
  99. commitId={commit.id}
  100. repository={commit.repository}
  101. onClick={onCommitClick ? () => onCommitClick(commit) : undefined}
  102. />
  103. ),
  104. date: (
  105. <TimeSince
  106. tooltipSuffix={commit.suspectCommitType}
  107. date={commit.dateCreated}
  108. />
  109. ),
  110. })}
  111. </Meta>
  112. </CommitMessage>
  113. {commit.pullRequest && commit.pullRequest.externalUrl && (
  114. <Button
  115. external
  116. href={commit.pullRequest.externalUrl}
  117. onClick={onPullRequestClick}
  118. >
  119. {t('View Pull Request')}
  120. </Button>
  121. )}
  122. </StyledPanelItem>
  123. );
  124. }
  125. const StyledPanelItem = styled(PanelItem)`
  126. display: flex;
  127. align-items: center;
  128. gap: ${space(2)};
  129. `;
  130. const AvatarWrapper = styled('div')`
  131. position: relative;
  132. `;
  133. const EmailWarning = styled('div')`
  134. font-size: ${p => p.theme.fontSizeSmall};
  135. line-height: 1.4;
  136. margin: -4px;
  137. `;
  138. const StyledLink = styled(Link)`
  139. color: ${p => p.theme.textColor};
  140. border-bottom: 1px dotted ${p => p.theme.textColor};
  141. &:hover {
  142. color: ${p => p.theme.textColor};
  143. }
  144. `;
  145. const EmailWarningIcon = styled('span')`
  146. position: absolute;
  147. bottom: -6px;
  148. right: -7px;
  149. line-height: 12px;
  150. border-radius: 50%;
  151. border: 1px solid ${p => p.theme.background};
  152. background: ${p => p.theme.yellow200};
  153. padding: 1px 2px 3px 2px;
  154. `;
  155. const CommitMessage = styled('div')`
  156. flex: 1;
  157. flex-direction: column;
  158. min-width: 0;
  159. margin-right: ${space(2)};
  160. `;
  161. const Message = styled(TextOverflow)`
  162. font-size: ${p => p.theme.fontSizeLarge};
  163. line-height: 1.2;
  164. `;
  165. const Meta = styled(TextOverflow)`
  166. font-size: 13px;
  167. line-height: 1.5;
  168. margin: 0;
  169. color: ${p => p.theme.subText};
  170. `;
  171. export {CommitRow};