feedbackListItem.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import {CSSProperties, forwardRef} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  5. import Checkbox from 'sentry/components/checkbox';
  6. import FeedbackItemUsername from 'sentry/components/feedback/feedbackItem/feedbackItemUsername';
  7. import useFeedbackHasReplayId from 'sentry/components/feedback/useFeedbackHasReplayId';
  8. import InteractionStateLayer from 'sentry/components/interactionStateLayer';
  9. import Link from 'sentry/components/links/link';
  10. import {Flex} from 'sentry/components/profiling/flex';
  11. import TextOverflow from 'sentry/components/textOverflow';
  12. import TimeSince from 'sentry/components/timeSince';
  13. import {IconCircleFill, IconPlay} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import {trackAnalytics} from 'sentry/utils/analytics';
  17. import {FeedbackIssue} from 'sentry/utils/feedback/types';
  18. import {decodeScalar} from 'sentry/utils/queryString';
  19. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  22. interface Props {
  23. feedbackItem: FeedbackIssue;
  24. isChecked: boolean;
  25. onChecked: (isChecked: boolean) => void;
  26. className?: string;
  27. style?: CSSProperties;
  28. }
  29. function useIsSelectedFeedback({feedbackItem}: {feedbackItem: FeedbackIssue}) {
  30. const {feedbackSlug} = useLocationQuery({
  31. fields: {feedbackSlug: decodeScalar},
  32. });
  33. const [, feedbackId] = feedbackSlug.split(':') ?? [];
  34. return feedbackId === feedbackItem.id;
  35. }
  36. const FeedbackListItem = forwardRef<HTMLDivElement, Props>(
  37. ({className, feedbackItem, isChecked, onChecked, style}: Props, ref) => {
  38. const organization = useOrganization();
  39. const isSelected = useIsSelectedFeedback({feedbackItem});
  40. const hasReplayId = useFeedbackHasReplayId({feedbackId: feedbackItem.id});
  41. return (
  42. <CardSpacing className={className} style={style} ref={ref}>
  43. <LinkedFeedbackCard
  44. data-selected={isSelected}
  45. to={() => {
  46. const location = browserHistory.getCurrentLocation();
  47. return {
  48. pathname: normalizeUrl(`/organizations/${organization.slug}/feedback/`),
  49. query: {
  50. ...location.query,
  51. referrer: 'feedback_list_page',
  52. feedbackSlug: `${feedbackItem.project.slug}:${feedbackItem.id}`,
  53. },
  54. };
  55. }}
  56. onClick={() => {
  57. trackAnalytics('feedback_list.details_link.click', {organization});
  58. }}
  59. >
  60. <InteractionStateLayer />
  61. <Flex column style={{gridArea: 'checkbox'}}>
  62. <Checkbox
  63. checked={isChecked}
  64. onChange={e => onChecked(e.target.checked)}
  65. onClick={e => e.stopPropagation()}
  66. />
  67. </Flex>
  68. <Flex column style={{gridArea: 'right'}}>
  69. {''}
  70. </Flex>
  71. <TextOverflow>
  72. <span style={{gridArea: 'user'}}>
  73. <FeedbackItemUsername feedbackItem={feedbackItem} detailDisplay={false} />
  74. </span>
  75. </TextOverflow>
  76. <span style={{gridArea: 'time'}}>
  77. <TimeSince date={feedbackItem.firstSeen} />
  78. </span>
  79. {feedbackItem.hasSeen ? null : (
  80. <span
  81. style={{
  82. gridArea: 'unread',
  83. display: 'flex',
  84. justifyContent: 'center',
  85. }}
  86. >
  87. <IconCircleFill size="xs" color="purple300" />
  88. </span>
  89. )}
  90. <div style={{gridArea: 'message'}}>
  91. <TextOverflow>{feedbackItem.metadata.message}</TextOverflow>
  92. </div>
  93. <Flex style={{gridArea: 'icons'}} gap={space(1)} align="center">
  94. <Flex align="center" gap={space(0.5)}>
  95. <ProjectAvatar project={feedbackItem.project} size={12} />
  96. {feedbackItem.project.slug}
  97. </Flex>
  98. {hasReplayId ? (
  99. <Flex align="center" gap={space(0.5)}>
  100. <IconPlay size="xs" />
  101. {t('Replay')}
  102. </Flex>
  103. ) : null}
  104. </Flex>
  105. </LinkedFeedbackCard>
  106. </CardSpacing>
  107. );
  108. }
  109. );
  110. const CardSpacing = styled('div')`
  111. padding: ${space(0.25)} ${space(0.5)};
  112. `;
  113. const LinkedFeedbackCard = styled(Link)`
  114. position: relative;
  115. border-radius: ${p => p.theme.borderRadius};
  116. padding: ${space(1)} ${space(1.5)} ${space(1)} ${space(1.5)};
  117. color: ${p => p.theme.textColor};
  118. &:hover {
  119. color: ${p => p.theme.textColor};
  120. }
  121. &[data-selected='true'] {
  122. background: ${p => p.theme.purple300};
  123. color: white;
  124. }
  125. display: grid;
  126. grid-template-columns: max-content 1fr max-content;
  127. grid-template-rows: max-content 1fr max-content;
  128. grid-template-areas:
  129. 'checkbox user time'
  130. 'unread message message'
  131. 'right icons icons';
  132. gap: ${space(1)};
  133. place-items: stretch;
  134. align-items: center;
  135. `;
  136. export default FeedbackListItem;