mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
234 lines
7.9 KiB
TypeScript
234 lines
7.9 KiB
TypeScript
import type {CSSProperties} from 'react';
|
|
import {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 type {Group} from 'sentry/types/group';
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
import type {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 isUserReportWithError = feedbackItem.metadata.source === 'user_report_envelope';
|
|
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('Has Activity')} containerDisplayMode="flex">
|
|
<IconChat color="gray500" size="sm" />
|
|
</Tooltip>
|
|
)}
|
|
|
|
{(isCrashReport || isUserReportWithError) && (
|
|
<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;
|