123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- import {CSSProperties, forwardRef} from 'react';
- import {browserHistory} from 'react-router';
- import {ThemeProvider} from '@emotion/react';
- import styled from '@emotion/styled';
- import ActorAvatar from 'sentry/components/avatar/actorAvatar';
- import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
- import Checkbox from 'sentry/components/checkbox';
- import FeedbackItemUsername from 'sentry/components/feedback/feedbackItem/feedbackItemUsername';
- import IssueTrackingSignals from 'sentry/components/feedback/list/issueTrackingSignals';
- import InteractionStateLayer from 'sentry/components/interactionStateLayer';
- import Link from 'sentry/components/links/link';
- import {Flex} from 'sentry/components/profiling/flex';
- import TextOverflow from 'sentry/components/textOverflow';
- import TimeSince from 'sentry/components/timeSince';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconChat, IconCircleFill, IconFatal, IconPlay} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import ConfigStore from 'sentry/stores/configStore';
- import {useLegacyStore} from 'sentry/stores/useLegacyStore';
- import {space} from 'sentry/styles/space';
- import {Group} from 'sentry/types/group';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {FeedbackIssue} from 'sentry/utils/feedback/types';
- import {decodeScalar} from 'sentry/utils/queryString';
- import useReplayCountForFeedbacks from 'sentry/utils/replayCount/useReplayCountForFeedbacks';
- import {darkTheme, lightTheme} from 'sentry/utils/theme';
- import useLocationQuery from 'sentry/utils/url/useLocationQuery';
- import useOrganization from 'sentry/utils/useOrganization';
- import {normalizeUrl} from 'sentry/utils/withDomainRequired';
- interface Props {
- feedbackItem: FeedbackIssue;
- isSelected: 'all-selected' | boolean;
- onSelect: (isSelected: boolean) => void;
- className?: string;
- style?: CSSProperties;
- }
- function useIsSelectedFeedback({feedbackItem}: {feedbackItem: FeedbackIssue}) {
- const {feedbackSlug} = useLocationQuery({
- fields: {feedbackSlug: decodeScalar},
- });
- const [, feedbackId] = feedbackSlug.split(':') ?? [];
- return feedbackId === feedbackItem.id;
- }
- const FeedbackListItem = forwardRef<HTMLDivElement, Props>(
- ({className, feedbackItem, isSelected, onSelect, style}: Props, ref) => {
- const config = useLegacyStore(ConfigStore);
- const organization = useOrganization();
- const isOpen = useIsSelectedFeedback({feedbackItem});
- const {feedbackHasReplay} = useReplayCountForFeedbacks();
- const hasReplayId = feedbackHasReplay(feedbackItem.id);
- const isCrashReport = feedbackItem.metadata.source === 'crash_report_embed_form';
- const hasComments = feedbackItem.numComments > 0;
- const theme = isOpen || config.theme === 'dark' ? darkTheme : lightTheme;
- return (
- <CardSpacing className={className} style={style} ref={ref}>
- <ThemeProvider theme={theme}>
- <LinkedFeedbackCard
- data-selected={isOpen}
- to={() => {
- const location = browserHistory.getCurrentLocation();
- return {
- pathname: normalizeUrl(`/organizations/${organization.slug}/feedback/`),
- query: {
- ...location.query,
- referrer: 'feedback_list_page',
- feedbackSlug: `${feedbackItem.project.slug}:${feedbackItem.id}`,
- },
- };
- }}
- onClick={() => {
- trackAnalytics('feedback.list-item-selected', {organization});
- }}
- >
- <InteractionStateLayer />
- <Row style={{gridArea: 'checkbox'}}>
- <Checkbox
- style={{gridArea: 'checkbox'}}
- disabled={isSelected === 'all-selected'}
- checked={isSelected !== false}
- onChange={e => {
- onSelect(e.target.checked);
- e.stopPropagation();
- }}
- invertColors={isOpen}
- />
- </Row>
- <TextOverflow style={{gridArea: 'user'}}>
- <FeedbackItemUsername feedbackIssue={feedbackItem} detailDisplay={false} />
- </TextOverflow>
- <TimeSince date={feedbackItem.firstSeen} style={{gridArea: 'time'}} />
- {feedbackItem.hasSeen ? null : (
- <DotRow style={{gridArea: 'unread'}}>
- <IconCircleFill size="xs" color={isOpen ? 'white' : 'purple400'} />
- </DotRow>
- )}
- <PreviewRow
- align="flex-start"
- justify="flex-start"
- style={{gridArea: 'message'}}
- isOpen={isOpen}
- >
- <StyledTextOverflow>{feedbackItem.metadata.message}</StyledTextOverflow>
- </PreviewRow>
- <BottomGrid style={{gridArea: 'bottom'}}>
- <Row justify="flex-start" gap={space(0.75)}>
- <StyledProjectAvatar
- project={feedbackItem.project}
- size={12}
- title={feedbackItem.project.slug}
- />
- <TextOverflow>{feedbackItem.shortId}</TextOverflow>
- </Row>
- <Row justify="flex-end" gap={space(1)}>
- <IssueTrackingSignals group={feedbackItem as unknown as Group} />
- {hasComments && (
- <Tooltip title={t('Linked Replay')} containerDisplayMode="flex">
- <IconChat color="gray500" size="sm" />
- </Tooltip>
- )}
- {isCrashReport && (
- <Tooltip title={t('Linked Error')} containerDisplayMode="flex">
- <IconFatal color="red400" size="xs" />
- </Tooltip>
- )}
- {hasReplayId && (
- <Tooltip title={t('Linked Replay')} containerDisplayMode="flex">
- <IconPlay size="xs" />
- </Tooltip>
- )}
- {feedbackItem.assignedTo && (
- <ActorAvatar
- actor={feedbackItem.assignedTo}
- size={16}
- tooltipOptions={{containerDisplayMode: 'flex'}}
- />
- )}
- </Row>
- </BottomGrid>
- </LinkedFeedbackCard>
- </ThemeProvider>
- </CardSpacing>
- );
- }
- );
- const CardSpacing = styled('div')`
- padding: ${space(0.25)} ${space(0.5)};
- `;
- const LinkedFeedbackCard = styled(Link)`
- position: relative;
- border-radius: ${p => p.theme.borderRadius};
- padding: ${space(1)} ${space(3)} ${space(1)} ${space(1.5)};
- color: ${p => p.theme.textColor};
- &:hover {
- color: ${p => p.theme.textColor};
- }
- &[data-selected='true'] {
- background: ${p => p.theme.purple300};
- color: white;
- }
- display: grid;
- grid-template-columns: max-content 1fr max-content;
- grid-template-rows: max-content 1fr max-content;
- grid-template-areas:
- 'checkbox user time'
- 'unread message message'
- '. bottom bottom';
- gap: ${space(1)};
- place-items: stretch;
- align-items: center;
- `;
- const Row = styled(Flex)`
- place-items: center;
- overflow: hidden;
- `;
- const BottomGrid = styled('div')`
- display: grid;
- grid-template-columns: auto max-content;
- gap: ${space(1)};
- overflow: hidden;
- `;
- const StyledProjectAvatar = styled(ProjectAvatar)`
- && img {
- box-shadow: none;
- }
- `;
- const PreviewRow = styled(Row)<{isOpen: boolean}>`
- height: 2.8em;
- align-items: flex-start;
- color: ${p => (p.isOpen ? p.theme.white : p.theme.gray300)};
- `;
- const DotRow = styled(Row)`
- height: 2.2em;
- align-items: flex-start;
- `;
- const StyledTextOverflow = styled(TextOverflow)`
- white-space: initial;
- height: 2.8em;
- -webkit-line-clamp: 2;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- line-height: ${p => p.theme.text.lineHeightBody};
- `;
- export default FeedbackListItem;
|