feedbackListItem.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import {CSSProperties, forwardRef} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {ThemeProvider} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import ActorAvatar from 'sentry/components/avatar/actorAvatar';
  6. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  7. import Checkbox from 'sentry/components/checkbox';
  8. import FeedbackItemUsername from 'sentry/components/feedback/feedbackItem/feedbackItemUsername';
  9. import IssueTrackingSignals from 'sentry/components/feedback/list/issueTrackingSignals';
  10. import InteractionStateLayer from 'sentry/components/interactionStateLayer';
  11. import Link from 'sentry/components/links/link';
  12. import {Flex} from 'sentry/components/profiling/flex';
  13. import TextOverflow from 'sentry/components/textOverflow';
  14. import TimeSince from 'sentry/components/timeSince';
  15. import {Tooltip} from 'sentry/components/tooltip';
  16. import {IconChat, IconCircleFill, IconFatal, IconPlay} from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import ConfigStore from 'sentry/stores/configStore';
  19. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  20. import {space} from 'sentry/styles/space';
  21. import {Group} from 'sentry/types/group';
  22. import {trackAnalytics} from 'sentry/utils/analytics';
  23. import {FeedbackIssue} from 'sentry/utils/feedback/types';
  24. import {decodeScalar} from 'sentry/utils/queryString';
  25. import useReplayCountForFeedbacks from 'sentry/utils/replayCount/useReplayCountForFeedbacks';
  26. import {darkTheme, lightTheme} from 'sentry/utils/theme';
  27. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  28. import useOrganization from 'sentry/utils/useOrganization';
  29. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  30. interface Props {
  31. feedbackItem: FeedbackIssue;
  32. isSelected: 'all-selected' | boolean;
  33. onSelect: (isSelected: boolean) => void;
  34. className?: string;
  35. style?: CSSProperties;
  36. }
  37. function useIsSelectedFeedback({feedbackItem}: {feedbackItem: FeedbackIssue}) {
  38. const {feedbackSlug} = useLocationQuery({
  39. fields: {feedbackSlug: decodeScalar},
  40. });
  41. const [, feedbackId] = feedbackSlug.split(':') ?? [];
  42. return feedbackId === feedbackItem.id;
  43. }
  44. const FeedbackListItem = forwardRef<HTMLDivElement, Props>(
  45. ({className, feedbackItem, isSelected, onSelect, style}: Props, ref) => {
  46. const config = useLegacyStore(ConfigStore);
  47. const organization = useOrganization();
  48. const isOpen = useIsSelectedFeedback({feedbackItem});
  49. const {feedbackHasReplay} = useReplayCountForFeedbacks();
  50. const hasReplayId = feedbackHasReplay(feedbackItem.id);
  51. const isCrashReport = feedbackItem.metadata.source === 'crash_report_embed_form';
  52. const hasComments = feedbackItem.numComments > 0;
  53. const theme = isOpen || config.theme === 'dark' ? darkTheme : lightTheme;
  54. return (
  55. <CardSpacing className={className} style={style} ref={ref}>
  56. <ThemeProvider theme={theme}>
  57. <LinkedFeedbackCard
  58. data-selected={isOpen}
  59. to={() => {
  60. const location = browserHistory.getCurrentLocation();
  61. return {
  62. pathname: normalizeUrl(`/organizations/${organization.slug}/feedback/`),
  63. query: {
  64. ...location.query,
  65. referrer: 'feedback_list_page',
  66. feedbackSlug: `${feedbackItem.project.slug}:${feedbackItem.id}`,
  67. },
  68. };
  69. }}
  70. onClick={() => {
  71. trackAnalytics('feedback.list-item-selected', {organization});
  72. }}
  73. >
  74. <InteractionStateLayer />
  75. <Row style={{gridArea: 'checkbox'}}>
  76. <Checkbox
  77. style={{gridArea: 'checkbox'}}
  78. disabled={isSelected === 'all-selected'}
  79. checked={isSelected !== false}
  80. onChange={e => {
  81. onSelect(e.target.checked);
  82. e.stopPropagation();
  83. }}
  84. invertColors={isOpen}
  85. />
  86. </Row>
  87. <TextOverflow style={{gridArea: 'user'}}>
  88. <FeedbackItemUsername feedbackIssue={feedbackItem} detailDisplay={false} />
  89. </TextOverflow>
  90. <TimeSince date={feedbackItem.firstSeen} style={{gridArea: 'time'}} />
  91. {feedbackItem.hasSeen ? null : (
  92. <DotRow style={{gridArea: 'unread'}}>
  93. <IconCircleFill size="xs" color={isOpen ? 'white' : 'purple400'} />
  94. </DotRow>
  95. )}
  96. <PreviewRow
  97. align="flex-start"
  98. justify="flex-start"
  99. style={{gridArea: 'message'}}
  100. isOpen={isOpen}
  101. >
  102. <StyledTextOverflow>{feedbackItem.metadata.message}</StyledTextOverflow>
  103. </PreviewRow>
  104. <BottomGrid style={{gridArea: 'bottom'}}>
  105. <Row justify="flex-start" gap={space(0.75)}>
  106. <StyledProjectAvatar
  107. project={feedbackItem.project}
  108. size={12}
  109. title={feedbackItem.project.slug}
  110. />
  111. <TextOverflow>{feedbackItem.shortId}</TextOverflow>
  112. </Row>
  113. <Row justify="flex-end" gap={space(1)}>
  114. <IssueTrackingSignals group={feedbackItem as unknown as Group} />
  115. {hasComments && (
  116. <Tooltip title={t('Linked Replay')} containerDisplayMode="flex">
  117. <IconChat color="gray500" size="sm" />
  118. </Tooltip>
  119. )}
  120. {isCrashReport && (
  121. <Tooltip title={t('Linked Error')} containerDisplayMode="flex">
  122. <IconFatal color="red400" size="xs" />
  123. </Tooltip>
  124. )}
  125. {hasReplayId && (
  126. <Tooltip title={t('Linked Replay')} containerDisplayMode="flex">
  127. <IconPlay size="xs" />
  128. </Tooltip>
  129. )}
  130. {feedbackItem.assignedTo && (
  131. <ActorAvatar
  132. actor={feedbackItem.assignedTo}
  133. size={16}
  134. tooltipOptions={{containerDisplayMode: 'flex'}}
  135. />
  136. )}
  137. </Row>
  138. </BottomGrid>
  139. </LinkedFeedbackCard>
  140. </ThemeProvider>
  141. </CardSpacing>
  142. );
  143. }
  144. );
  145. const CardSpacing = styled('div')`
  146. padding: ${space(0.25)} ${space(0.5)};
  147. `;
  148. const LinkedFeedbackCard = styled(Link)`
  149. position: relative;
  150. border-radius: ${p => p.theme.borderRadius};
  151. padding: ${space(1)} ${space(3)} ${space(1)} ${space(1.5)};
  152. color: ${p => p.theme.textColor};
  153. &:hover {
  154. color: ${p => p.theme.textColor};
  155. }
  156. &[data-selected='true'] {
  157. background: ${p => p.theme.purple300};
  158. color: white;
  159. }
  160. display: grid;
  161. grid-template-columns: max-content 1fr max-content;
  162. grid-template-rows: max-content 1fr max-content;
  163. grid-template-areas:
  164. 'checkbox user time'
  165. 'unread message message'
  166. '. bottom bottom';
  167. gap: ${space(1)};
  168. place-items: stretch;
  169. align-items: center;
  170. `;
  171. const Row = styled(Flex)`
  172. place-items: center;
  173. overflow: hidden;
  174. `;
  175. const BottomGrid = styled('div')`
  176. display: grid;
  177. grid-template-columns: auto max-content;
  178. gap: ${space(1)};
  179. overflow: hidden;
  180. `;
  181. const StyledProjectAvatar = styled(ProjectAvatar)`
  182. && img {
  183. box-shadow: none;
  184. }
  185. `;
  186. const PreviewRow = styled(Row)<{isOpen: boolean}>`
  187. height: 2.8em;
  188. align-items: flex-start;
  189. color: ${p => (p.isOpen ? p.theme.white : p.theme.gray300)};
  190. `;
  191. const DotRow = styled(Row)`
  192. height: 2.2em;
  193. align-items: flex-start;
  194. `;
  195. const StyledTextOverflow = styled(TextOverflow)`
  196. white-space: initial;
  197. height: 2.8em;
  198. -webkit-line-clamp: 2;
  199. display: -webkit-box;
  200. -webkit-box-orient: vertical;
  201. line-height: ${p => p.theme.text.lineHeightBody};
  202. `;
  203. export default FeedbackListItem;